There are twenty-three patterns in the catalogue that made the idea famous. You will use perhaps eight of them regularly, and applying the other fifteen without a genuine reason will make your code worse.
A design pattern is a named solution to a problem that recurs. The naming is half the value: saying "that is a Strategy" hands someone a whole structure — the participants, how they collaborate, what varies and what stays fixed — in a single word.
The failure mode is treating the catalogue as a to-do list. Patterns are answers. If you have not felt the question, you do not need the answer.
What a pattern is, and what it is not
Four things get called "design" and they sit at different altitudes. Confusing them is why people argue past each other about whether a pattern is "overkill".
Diagram, as text
- Principle always true, no structure depend on abstractions leads to Pattern a structure for one recurring problem Strategy, Adapter, Observer (gives the reason for)
- Architectural style shape of a whole system layered, hexagonal, pipes leads to Pattern a structure for one recurring problem Strategy, Adapter, Observer (constrains which)
- Pattern a structure for one recurring problem Strategy, Adapter, Observer leads to Idiom the same idea in one language a lookup of functions (written in code as)
A principle is a direction, not a shape. "Depend on abstractions, not concretions" tells you which way an arrow should point. It does not tell you how many classes to write. Principles are covered in low-level design under SOLID, and the next topic here covers the ones that specifically produce patterns.
A pattern is a structure with a name. It names the participants and their collaboration for one recurring problem. It is specific enough to draw and general enough to reuse.
An idiom is a pattern rendered in one language. Strategy in Java is an interface and a few classes; in JavaScript it is usually an object of functions. Same pattern, different idiom — and the second one is not "cheating".
An architectural style is the shape of a whole system. Layered, hexagonal, event-driven. It decides what talks to what across a codebase, and it constrains which patterns you will need inside. The last section of this module covers these.
The three families
The classic catalogue sorts patterns by what they are about. The sorting is genuinely useful, because it maps to the question you were asking when you went looking.
| Family | The question it answers | Examples |
|---|---|---|
| Creational | How does this object get made, and who decides which kind? | Factory Method, Builder, Prototype |
| Structural | How do these objects fit together and present themselves? | Adapter, Decorator, Facade, Proxy |
| Behavioural | How do these objects divide work and talk to each other? | Strategy, Observer, Command, State |
Two more families arrived later and are covered here too, because production code needs them:
Concurrency patterns answer "how do these objects share work safely across threads?" — producer–consumer, thread pools, immutable values, actors.
Architectural patterns answer "how is the whole application arranged?" — layered and hexagonal architecture, the model–view family, and the data-access patterns.
A pattern is a trade, not a free upgrade
Every pattern buys flexibility along one specific axis and charges indirection for it. That is the whole deal, and it is why "should I use a pattern here?" is never answered by "patterns are good practice".
What you pay, every time:
- A reader must follow one more hop. The thing that runs is no longer the thing you called.
- A name enters the codebase. If nobody recognises it, it reads as unexplained ceremony rather than shared vocabulary.
- The abstraction commits you. An interface designed against one implementation almost always guesses wrong about what actually varies.
What you buy, when it applies:
- New variants stop editing old code. Adding the fourth payment provider touches one new file rather than seven existing ones.
- The variation gets a name. "Renderer" tells the next reader what the axis of change is.
- Testing gets a seam. Substituting a fake at the boundary is the point of the boundary.
The trade is good when the flexibility is used and bad when it is not. That is a fact about your problem, not about the pattern.
How this module is organised
Seven sections, in the order they build on each other.
| Section | What it covers |
|---|---|
| Foundations | This topic, the principles that generate patterns, and reading the diagrams |
| Creational | Factory Method, Abstract Factory, Builder, Prototype, Singleton, Object Pool |
| Structural | Adapter, Decorator, Facade, Proxy, Composite, Bridge, Flyweight |
| Behavioural | Strategy, Observer, Command, State, Template Method, Chain of Responsibility, Iterator, Mediator, Memento, Visitor, Interpreter |
| Concurrency | Producer–consumer, thread pools, immutability and safe publication, actors |
| Architectural | Layered and hexagonal, the model–view family, data access, domain events and CQRS |
| Applying them | Anti-patterns, refactoring toward patterns, and a worked case study |
Each topic follows the same shape: the problem first, in code that hurts; then the pattern; then when it does not apply. Read the problem before the solution — the solution is not memorable without it.
When not to reach for a pattern
You have one case. A Strategy with a single implementation is indirection with no benefit. The second implementation teaches you the interface; the first one cannot.
The pattern is longer than the problem. If applying it triples the code to save a hypothetical future edit, you have paid a certain cost for an uncertain gain.
The language already solves it. Many patterns exist to work around limitations of 1990s C++ and Java. In a language with first-class functions, Strategy is a function argument and Command is a closure. Reaching for a class hierarchy is often ceremony.
Nobody on the team knows the name. Half a pattern's value is shared vocabulary. Used unnamed and unrecognised, it reads as unnecessary abstraction — and gets refactored away by the next person.
Check your understanding
Common mistakes
Reaching for a pattern before there is a problem. A pattern is a response to a specific pressure — something that varies, something that must be swapped, something that changes together. Applied without that pressure it is pure overhead: three classes and an interface doing the work of one function.
Treating the names as the knowledge. Being able to recite the Strategy pattern is not the same as recognising, in unfamiliar code, that a growing switch is a strategy waiting to be extracted. Interviews probe the second; only the first can be memorised.
Assuming a pattern is always an improvement. Indirection has a cost: every layer is another hop a reader has to follow to answer "what actually happens here". A pattern pays for that cost when the flexibility is used, and does not when it is not.
Forcing one language's version onto another. Several classic patterns exist to work around the absence of first-class functions. In a language that has them, the faithful class-based version is ceremony around something the language already does.
Letting patterns dictate the domain vocabulary. A class called OrderProcessingStrategyFactory names our solution, not the customer's world. The domain should still be legible after the patterns are applied.
Stopping at the catalogue. The patterns worth knowing are outnumbered by the principles behind them. Someone who understands why a stable abstraction should not depend on a volatile one can derive half the catalogue; someone who memorised the catalogue cannot derive the principle.
Practice
A teammate has read the catalogue and opened a pull request that adds a Factory, a Strategy and an Observer to a module that has one implementation of each thing and no listeners. Nothing is broken. Write the review comment.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Separate what the patterns cost from whether they are correctly implemented. The code is not wrong; it is unpaid-for.
Show a strong answer
Start by conceding the code is correct, because it is, and a review that opens with "this is wrong" will get an argument about whether the pattern was implemented properly — which is not the issue.
Then name the cost concretely, not abstractly. Vague objections ("this is over-engineered") lose. Specific ones land:
- Finding what runs now takes three hops. A reader chasing a bug follows the caller to the factory, the factory to the interface, the interface to the single implementation. That cost is paid on every future read, by everyone.
- The interface is a guess. With one implementation, the shape was derived from that implementation. When the second arrives it will not fit, and reshaping it will now touch the factory, the interface and every caller — more work than if the abstraction had not existed.
- The Observer has no subscribers. An event with no listeners is a function call with the call site deleted. Failures and ordering are now implicit for no gain.
Propose the concrete alternative, which is not "delete all abstraction":
- Keep the logic behind one narrow entry point. Containment is cheap and always pays; it is what makes the later extraction a small change.
- Do not leak the implementation's vocabulary into callers. If callers speak in domain terms, the internals stay free to change.
That is the useful distinction: encapsulation now, abstraction later.
Say what would change your mind, so the review is a question rather than a verdict: is a second implementation actually funded and scheduled? "We might need it" is a guess; a start date is a fact, and a fact would make this pull request correct.
For each, say whether it is a principle, a pattern, an idiom or an architectural style, and why the distinction matters in that case: (a) 'depend on abstractions, not concretions'; (b) the domain layer must not import the database driver; (c) passing a comparison function to a sort routine; (d) wrapping a vendor SDK in a class that exposes your own method names.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Ask what the scope of each one is — a line, a module, or the whole application — and how many times it appears in a codebase.
Show a strong answer
(a) A principle. It states a direction for dependencies and nothing about structure. It is the reason several patterns exist, but on its own it cannot be implemented — you cannot open a file and write "depend on abstractions".
Why it matters: principles cannot be over-applied in the way patterns can. Someone objecting to "too much abstraction" is objecting to a structure, so the conversation has to move down an altitude to be productive.
(b) An architectural style — this is the dependency rule of layered and hexagonal architecture. Its scope is the whole application, it is enforced once (by review, by lint rules or by module boundaries), and it constrains what patterns you need inside: forbidding that import is precisely what creates demand for Adapter and Repository at the boundary.
Why it matters: it is not negotiable per file. A pattern is a local judgement call; a style is a global invariant, and allowing exceptions "just here" is how it stops holding.
(c) An idiom — and the pattern behind it is Strategy. The comparison function is an interchangeable behaviour supplied by the caller, which is exactly Strategy's structure, spelled the way a language with first-class functions spells it.
Why it matters: recognising it as Strategy stops you from "upgrading" working code into a Comparator interface with three implementing classes. The pattern is already there.
(d) A pattern — Adapter. It is local, it appears once per foreign interface, and its participants are clear: your expected interface, the vendor's actual one, and the translator between them.
Why it matters: naming it Adapter sets the expectation that it stays thin. A class that is "the vendor wrapper" tends to accumulate business logic; a class that is "the Adapter" invites the reviewer to ask why domain rules are living in a translator.
Key takeaways
- A pattern is a named solution to a recurring problem. The shared vocabulary is half its value.
- Principle, pattern, idiom and architectural style sit at different altitudes. Most arguments about over-engineering are two people talking about different ones.
- The classic catalogue sorts patterns into creational, structural and behavioural; concurrency and architectural patterns were added later and matter just as much in production code.
- Every pattern buys flexibility along one axis and charges indirection. The trade is good only when the flexibility is actually used.
- Wait for the second implementation. It teaches you the interface; the first one can only make you guess.
- Encapsulation now, abstraction later. Containing logic behind one entry point is cheap; a premature interface is a guess you are committed to.
Further reading
- Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
- Christopher Alexander, A Pattern Language (Oxford University Press, 1977) — the origin of the idea of a pattern.
- Joshua Kerievsky, Refactoring to Patterns (Addison-Wesley, 2004) — arriving at a pattern rather than starting with one.
- Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002).
- Frank Buschmann, Regine Meunier, Hans Rohnert, Peter Sommerlad and Michael Stal, Pattern-Oriented Software Architecture, Volume 1 (Wiley, 1996) — where the style/pattern/idiom distinction is set out.
- Brian Foote and Joseph Yoder, "Big Ball of Mud" (Pattern Languages of Program Design 4, 1999).