Loading learning content...
The most dangerous words in software architecture are: "Let's use the Strategy pattern here." Not because Strategy is a bad pattern—it's excellent. The danger lies in starting with a pattern instead of starting with a problem.
Experienced engineers understand a counterintuitive truth: the hardest part of applying design patterns isn't understanding the patterns themselves—it's understanding the problem well enough to know which pattern applies. A developer who memorizes all 23 Gang of Four patterns but can't accurately diagnose problems will consistently make poor design decisions. Conversely, an engineer who deeply understands problem identification but knows fewer patterns will make better choices with the patterns they know.
By the end of this page, you will know how to systematically identify and articulate design problems before selecting patterns. You'll learn frameworks for problem decomposition, symptom-vs-cause analysis, and how to recognize the specific problem characteristics that signal which patterns might apply.
Pattern selection failures almost always trace back to problem identification failures. Consider these real-world scenarios:
Scenario A: The Misapplied Factory
A team notices they have similar code for creating User, Admin, and Guest objects. Someone suggests: "We need a Factory pattern!" They invest two weeks building an elaborate Abstract Factory hierarchy. Later, they realize the actual problem was that all three types should have been a single User class with a role field. The Factory didn't solve a problem—it institutionalized a modeling mistake.
Scenario B: The Overcomplicated Command
A developer sees code that needs undo/redo functionality. "Classic Command pattern!" they declare. They build a full Command infrastructure with invokers, receivers, and command queues. The actual requirement? Users needed to undo only the last action, and a simple memento of previous state would have sufficed. The Command pattern solved the general undo problem, not their specific undo problem.
When you start with patterns, you filter problems through pattern-shaped lenses. You see everything as nails because you're holding a hammer. Problem-first thinking inverts this: understand the exact shape of your problem, then find the tool that matches.
The Cost of Misidentification
Misidentified problems lead to:
The first step in pattern selection isn't pattern knowledge—it's problem knowledge.
Effective problem identification follows a structured approach. Before considering any pattern, systematically answer these five questions:
Let's apply this framework to a concrete example:
Scenario: Payment Processing System
Your e-commerce platform currently supports credit cards. Now you need to add PayPal, Apple Pay, Google Pay, and cryptocurrency. The current codebase has payment logic interleaved throughout the checkout process.
| Question | Analysis |
|---|---|
| 1. Observable Symptom | Adding new payment methods requires modifying multiple files across checkout flow. Each change risks breaking existing payments. |
| 2. Underlying Cause | Payment logic is not encapsulated. The checkout process directly calls payment-specific code rather than working through an abstraction. |
| 3. Forces/Constraints | Need to add payment methods without modifying stable checkout code. Each payment provider has different APIs and authentication flows. |
| 4. Desired Outcome | Adding a new payment method should require adding new code, not changing existing code. Checkout should be payment-method agnostic. |
| 5. Must Remain Unchanged | Transaction logging, error handling patterns, customer communication. Payment processing latency SLA (< 3 seconds). |
Notice how completing this analysis naturally suggests the pattern direction: we need to encapsulate varying payment implementations behind a stable interface. We haven't named a pattern yet, but the solution shape is becoming clear. This is problem-first thinking at work.
The most common problem identification error is treating symptoms as causes. This is understandable—symptoms are visible, causes are hidden. But solutions that address symptoms without reaching causes provide temporary relief that often creates new problems.
The Medical Analogy
A patient presents with a persistent cough. Prescribing cough suppressants addresses the symptom but not underlying pneumonia. The correct approach: investigate the cough's cause, then treat appropriately. Software design works identically.
Common Symptom-Cause Confusions in Software
| Visible Symptom | Superficial Diagnosis | Actual Root Cause | Real Solution Direction |
|---|---|---|---|
| Class has 2000 lines | "Class is too big, split it" | Class has multiple responsibilities that grew together organically | Identify distinct responsibilities, extract classes by cohesion |
Lots of if/else chains | "Use polymorphism" | Switching on type rather than delegating behavior to types | Move behavior to type classes via Strategy or State |
| Duplicate code across classes | "Extract common base class" | Similar operations, but distinct conceptual types | Compose via delegation or shared components, not inheritance |
| Changes ripple across many files | "Decouple with interfaces" | Modules know too much about each other's internals | Enforce information hiding, define stable public contracts |
| Hard to test classes | "Add mocking framework" | Classes create their own dependencies | Apply Dependency Injection, design for testability |
The Five Whys Technique
Borrowed from lean manufacturing, the Five Whys helps trace symptoms to root causes:
Symptom: Adding a new report type takes two weeks.
Why modify the base class? Because all report types inherit from it and share behavior there.
Why is shared behavior in the base class? Because that was the easiest place to put common formatting logic.
Why does formatting live with report generation? Because we didn't separate concerns—generation and presentation are coupled.
Why are they coupled? Because the original design assumed one output format; now we have five.
Root Cause Revealed: Report generation and presentation are incorrectly coupled. New report types or new formats both require base class changes.
Solution Direction: Separate report data generation from report presentation. Use something like Visitor or Strategy to vary presentation independently from structure.
Notice: we didn't start with "let's use Visitor." We arrived at Visitor-like solutions by drilling into the actual cause.
Five is not a magic number. Sometimes you need three whys; sometimes you need seven. The point is to keep asking until you reach a cause that, if changed, would eliminate the symptom chain above it.
Every design problem exists because of forces—constraints, requirements, or goals that pull the design in different directions. Understanding forces is essential because:
Categories of Design Forces
Force Analysis Example: Notification System
You're designing a notification system that sends alerts via email, SMS, push notification, and Slack. Let's analyze the forces:
Force 1: Variability in Channels
Force 2: Consistency in Business Logic
Force 3: Reliability Requirements
Force 4: Testability
Force Tension Identified: We need variation at the channel level but consistency at the business logic level. We need to isolate channels for testing but integrate them for operation.
This force analysis points toward patterns that:
Again—we haven't picked patterns yet. But force analysis makes the solution space concrete.
Write down the forces explicitly in design documents. This creates alignment across the team and provides rationale for future maintainers who wonder why a particular pattern was chosen.
After identifying your problem's root cause and forces, the next step is categorizing the problem type. Problems cluster into recognizable families, and pattern families align with problem families.
This is not about memorizing which pattern solves which problem. It's about recognizing that your problem belongs to a problem family, which narrows pattern candidates from 23+ to 3-5 relevant options.
| Problem Family | Core Tension | Pattern Candidates |
|---|---|---|
| Object Creation Complexity | Need to create objects without tightly coupling to concrete types | Factory Method, Abstract Factory, Builder, Prototype |
| Algorithm Variation | Same structure, but configurable/swappable behavior | Strategy, Template Method, State |
| Object Composition | Need to combine objects into larger structures flexibly | Composite, Decorator, Chain of Responsibility |
| Interface Adaptation | Need to make incompatible interfaces work together | Adapter, Facade, Bridge |
| State Management | Object behavior depends on state; state transitions are complex | State, Memento |
| Communication/Notification | Objects need to communicate without tight coupling | Observer, Mediator, Command |
| Access Control | Need to control or augment access to objects | Proxy, Flyweight, Singleton |
| Traversal/Iteration | Need to traverse structures without exposing internals | Iterator, Visitor |
Using Problem Families in Practice
Let's say your problem analysis reveals:
Categorization: This is an Algorithm Variation problem. The report structure is fixed; the rendering algorithm varies.
Candidate patterns: Strategy, Template Method, Visitor
Now you can compare 3 patterns against your specific constraints instead of evaluating all 23. The next page covers how to evaluate pattern fit—but you've already reduced your search space dramatically through proper problem identification.
Some patterns address multiple problem families. Decorator handles both object composition and algorithm variation. This is why problem categorization narrows candidates but doesn't immediately select the answer—you still need evaluation.
A well-crafted problem statement ensures clarity and alignment. Vague statements like "the code is messy" or "we need better architecture" provide no actionable direction. Use structured templates to articulate problems precisely.
Template 1: The Force-Resolution Statement
We need to [achieve goal] while [force 1] AND [force 2].
Currently, [current approach] fails because [reason].
A solution must [constraint 1] and [constraint 2].
Example:
We need to support multiple payment providers while keeping checkout code stable AND maintaining consistent transaction logging. Currently, hardcoded payment logic in checkout fails because each new provider requires modifying checkout files. A solution must allow adding providers as new code and preserve existing logging/error handling.
Template 2: The Symptom-Cause-Desire Statement
Observed: [symptom]
Cause: [root cause after analysis]
Desired: [outcome stated without solution]
Constraints: [what must be preserved]
Example:
Observed: Adding new report types takes 2 weeks and requires touching 15 files. Cause: Report generation, formatting, and export are entangled in a single inheritance hierarchy. Desired: Adding a new report type should require creating one new class with no changes to existing code. Constraints: Existing reports must continue working; shared formatting utilities must remain accessible.
The act of writing forces precision. When you can't articulate the problem clearly in writing, you don't understand it well enough to select a pattern. Use problem statement writing as a self-test for problem understanding.
Template 3: The Pattern-Agnostic Requirements
For complex problems, enumerate concrete requirements:
1. [Requirement]: [Specific, measurable criterion]
2. [Requirement]: [Specific, measurable criterion]
...
Non-requirements (explicitly out of scope):
- [What this solution is NOT required to do]
Example:
Requirements for Notification System Redesign:
- Channel Independence: New channels addable without modifying core notification logic
- Consistent Logging: All notification attempts logged uniformly regardless of channel
- Preference Enforcement: User notification preferences checked before any send
- Testability: Channels testable in isolation with mock replacements
- Failure Handling: Failed sends queued for retry with configurable backoff
Non-requirements:
- Real-time notification tracking (different project)
- Admin UI for channel configuration (will use config files initially)
- Multi-language notification templates (English only for MVP)
With this requirements list, you can evaluate any proposed pattern against concrete criteria rather than vague impressions.
Even with good frameworks, problem identification can go wrong. Awareness of common pitfalls helps avoid them.
If your problem statement could apply to almost any codebase, it's too vague. Good problem statements are specific enough that someone unfamiliar with your code could understand what needs fixing.
Problem identification is the foundation of pattern selection. Before any pattern enters your thinking, you must deeply understand what you're solving.
What's Next:
With your problem properly identified, the next page covers Evaluating Pattern Fit—how to take your narrowed candidate list and systematically determine which pattern (if any) best addresses your specific problem and constraints.
You now understand that effective pattern selection begins long before any pattern is named. Problem identification—through symptom-cause analysis, force identification, and precise problem statements—creates the foundation for choosing the right pattern. Next, we'll explore how to evaluate candidate patterns against your identified problem.