Design Patterns Best Practices
Knowing a design pattern is the first step. Using it well is the second. Many developers learn patterns from textbooks and then either over-apply them or reach for the wrong one. This page covers the principles, decision rules, and habits that separate confident pattern use from cargo-cult engineering.
Understand the Problem Before Picking a Pattern
Every pattern solves a specific category of problem. Before reaching for a pattern, name the problem in one sentence. If you cannot describe the pain point clearly, you do not yet know which pattern fits.
Problem: "Multiple parts of the UI must stay in sync
with one shared data model."
Pattern: Observer
Problem: "I need to swap algorithms at runtime
without if-else chains."
Pattern: Strategy
Problem: "I need to undo and replay user actions."
Pattern: Command
Pattern names are shorthand for recurring solutions. Use them as vocabulary with your team, not as a checklist to complete.
SOLID Principles and How Patterns Support Them
Design patterns are practical expressions of the SOLID principles. Knowing the connection helps you choose the right pattern and explain it to others.
Single Responsibility Principle
A class should have one reason to change. Command encapsulates one action per class. Iterator separates traversal logic from the collection. Both keep each class focused on one job.
Open/Closed Principle
Code should be open for extension but closed for modification. Strategy and Observer let you add new behaviors (new strategies, new observers) without changing existing classes.
Liskov Substitution Principle
Subtypes must work wherever the parent type is expected. Every concrete strategy must honor the contract of the strategy interface. Every concrete observer must honor the observer interface without surprising the Subject.
Interface Segregation Principle
Clients should not depend on methods they do not use. Iterator exposes only hasNext() and next() — a clean, minimal interface. Avoid bloating interfaces with methods only some implementations use.
Dependency Inversion Principle
High-level modules should depend on abstractions, not concrete classes. The Invoker in Command depends on the Command interface, not on BoldCommand or DeleteCommand. The Context in Strategy depends on SortStrategy, not on BubbleSort.
When Not to Use a Pattern
Patterns add structure. Structure costs: more files, more indirection, more reading effort for new team members. Apply a pattern only when the complexity it introduces is less than the complexity it removes.
Signals That a Pattern Is Premature
- You have one algorithm and no expectation of a second — Strategy is unnecessary.
- You have one observer and no plan to add more — Observer adds boilerplate for no gain.
- The feature will not need undo, queueing, or logging — Command wraps a call that did not need wrapping.
- You are adding a pattern "just in case" — design for current requirements, refactor when the need arrives.
The Two-Instance Rule
A practical trigger: consider extracting a pattern when you see a second concrete variation of something. One payment method needs no Strategy. Two payment methods and a third arriving next month — add Strategy now. One observer stretches the pattern thin. Three observers and a growing team confirm it.
Composing Patterns Together
Real systems rarely use one pattern in isolation. Patterns combine naturally when the problem is large enough.
Observer + Command
User clicks button | v Command object created (InsertTextCommand) | v Invoker executes the command | v Subject state changes (TextEditor content updates) | v Observer notified (WordCountDisplay refreshes)
The Command handles the action and its undo. The Observer keeps the display in sync with the result. Neither pattern knows about the other.
Strategy + Iterator
A report generator uses: - Strategy to select the output format (PDF, CSV, HTML) - Iterator to walk through the report's data rows The iterator does not know the format. The format strategy does not know the data source. Both can be replaced independently.
Naming and Communication
Use pattern names in code comments, pull request descriptions, and architecture diagrams. When you write // Invoker — stores commands for undo, the next developer immediately understands the design intent without reading every line.
// BAD: vague variable name Object h = new EditorHistory(); // GOOD: communicates the pattern role EditorHistory commandInvoker = new EditorHistory();
Class names can also carry the pattern intent: PaymentStrategyContext, TemperatureSubject, LogCommand. A reader learns the pattern role from the name alone.
Testing Patterns in Isolation
Patterns introduce interfaces and injection points. These are excellent seams for unit tests.
Testing Strategy
Inject a mock strategy into the context and assert that the context calls sort() exactly once with the correct data. Test each concrete strategy independently with known inputs and expected outputs.
Testing Observer
Use a mock observer that records every update() call. Change the subject's state, then assert the mock observer received the correct value the right number of times.
Testing Command
Execute a command, assert the receiver's state changed. Call undo(), assert the receiver returned to its prior state. Test macro commands by verifying that each child command received its call.
// Example: testing undo
TextEditor editor = new TextEditor();
Command cmd = new InsertTextCommand(editor, "Hello");
cmd.execute();
assert editor.getText().equals("Hello");
cmd.undo();
assert editor.getText().equals("");
Avoiding Pattern Rot
Pattern rot happens when a pattern that made sense at the start becomes a burden as the system grows. Watch for these signs:
Observer Fan-Out Overload
A subject notifies fifty observers on every keystroke. Performance degrades. Consider batching notifications, using a dirty flag to coalesce rapid changes, or moving to an async event bus for high-frequency updates.
Strategy Explosion
Forty concrete strategy classes for minor variations signal that the variation should be controlled by configuration or data, not by separate classes. Consolidate related strategies into one parameterized class.
Command History Memory Leak
Storing every command forever grows memory without bound. Enforce a maximum history depth. Discard old commands beyond that limit.
Refactoring Toward Patterns
You do not always design with patterns from the start. Often, you refactor toward them when code starts showing specific warning signs.
Warning sign Likely pattern to introduce ---------------------------- ---------------------------- Growing if-else on type/mode Strategy Manual notify calls everywhere Observer No undo support but users need it Command Custom loops per collection type Iterator
Refactor in small steps. Extract an interface first. Then move the implementation into a concrete class. Then inject it into the context. Each step is testable and reversible.
Documentation and Onboarding
A pattern is only valuable if your team understands it. Document the pattern decisions in your project's architecture notes:
- Which pattern is used and where.
- The problem it was introduced to solve.
- What the extension points are (where to add a new strategy, observer, or command).
- Any constraints or known limitations in this implementation.
New developers on the team read this first. They can contribute a new payment strategy or a new observer without digging through the entire codebase.
Pattern Maturity Checklist
Before calling a pattern "done," verify each item:
- The problem this pattern solves is documented in a comment or ADR (Architecture Decision Record).
- Every role (Subject, Observer, Strategy, Command, etc.) maps to a clearly named class or interface.
- Unit tests cover execute and undo paths (for Command) or multiple concrete implementations (for Strategy, Observer).
- The extension point is clear: a new developer can add a new concrete class without modifying existing ones.
- No pattern is introduced "just in case" — a real, current need justifies each one.
Benefits of Disciplined Pattern Use
- Teams share vocabulary: "add a new Strategy" communicates a full plan in three words.
- Code grows by addition (new classes) rather than by modification (touching existing classes).
- Testing is easier because each role is isolated behind an interface.
- Onboarding is faster when architecture follows recognizable, well-documented patterns.
- Refactoring is safer because the pattern's boundaries contain the blast radius of changes.
Quick Summary
Design patterns are tools, not goals. Identify the problem first. Choose the pattern that removes more complexity than it adds. Compose patterns when the system is large enough to justify it. Name your roles clearly, write tests at the seams, and document your decisions. The best pattern in a codebase is the one every team member understands and can extend confidently.
