PatternsPARADIGM-SPECIFICCONTESTEDSCALE-SPECIFIC

Patterns as Vocabulary

Their durable value is a shared name for a shape you already built. Treated as a construction kit instead of a naming scheme, they add structure nobody needed.

The requirement, the obvious build, and why it breaks

Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.

The question

What are design patterns actually for, given that most codebases would be worse if they contained more of them?

The requirement

A new engineer joins and asks why the codebase has no patterns in it. A senior engineer says the codebase is full of them and they simply do not have the names on.

The obvious build

Learn the twenty-three patterns and apply them. They are proven solutions from experienced engineers, so a codebase with more of them in it is a better-designed codebase.

Why it breaks

The catalogue is a list of solutions with the problems left implicit, so learning it as a kit produces engineers who match solutions to nothing in particular (Premature Abstraction).

How it breaks as requirements change
  • The catalogue is a list of solutions with the problems left implicit, so learning it as a kit produces engineers who match solutions to nothing in particular (Premature Abstraction).
  • About half the original catalogue exists to work around missing language features. Strategy, Command and Template Method are function values; Iterator is for..of; Singleton is a module. Applying them in a language that has the feature adds classes and subtracts nothing (Strategy).
  • The vocabulary drifts. "Factory" means four different things in four codebases, and a review comment saying "use a factory" now costs a conversation instead of saving one.
  • Pattern names become authority. "It is the observer pattern" ends a discussion that should have been about whether the indirection is worth its traceability cost (Tone, Disagreement and Receiving Review).
  • The counting failure: a design is judged by how many patterns are visible in it, which is a metric nobody would defend out loud and many teams use in practice.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

What limits the solution, and what must never stop being true

This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.

Constraints
  • The team is six people with mixed backgrounds; three learned patterns from the Gang of Four book, two from a framework, one not at all.
  • Code review is the main channel for design discussion, so the vocabulary has to work in a comment thread.
  • The codebase is a five-year-old business system, not a framework or library (Library or Framework).
Invariants
  • A name used in review must mean the same thing to everyone reading it, or it is worse than no name.
  • Naming a shape must never become an argument for adopting it (Pattern Overuse).

Who owns what, and where the seams fall

Responsibilities decide boundaries; boundaries decide what an interface has to say.

Responsibilities
  • The vocabulary owns communication. Its job is to compress a paragraph of description into one word that both parties decode identically.
  • The engineer owns the decision. The name never decides; the observed problem and the change cost decide (Changeability Is the Goal).
  • The team owns its own glossary where the canonical names are ambiguous — writing down what *this* codebase means by "repository" is worth more than a citation (Ubiquitous Language).
Boundaries
  • A pattern name belongs in a code review comment, an ADR and a class name. It does not belong in a requirement or an estimate (Architecture Decision Records).
  • The boundary between naming and prescribing is the one that matters: "this is a decorator" is an observation, "this should be a decorator" is a design proposal that needs the usual argument.
  • A name is worth attaching when the shape already exists. Naming a shape you are about to build is how the name becomes the reason (Speculative Generality).

What the name is worth, and what it costs

Read this as a two-column ledger rather than a recommendation. Every row has a real benefit in the middle column and a real, commonly-realised failure in the right one, and the failure is almost always the same failure: the name arrived before the problem.

  • The left column is worth learning; the middle column is what to say out loud instead of the left column.
  • Every right-column reading turns a conditional judgement into an unconditional rule, which is the specific way this vocabulary fails.
  • Notice that Adapter and Observer describe *boundaries*, and those two have aged far better than the ones describing object construction.
Pattern nameWhat it usefully compressesWhat people hear instead
Strategy"Behaviour varies by a kind that recurs, and the set is open""Wrap every conditional in an interface" (Strategy)
Factory"Construction has grown a decision that callers should not make""Never call a constructor" (Factory)
Adapter"Their shape stops here; ours starts here""Wrap every library" (Adapter)
Observer"The producer must not know its consumers""Events are decoupled, therefore better" (Observer)
Decorator"Add behaviour without changing the thing or its callers""Wrap it once more" (Decorator)
Singleton"Exactly one instance, globally reachable"A global variable with a design-pattern alibi (Hidden Global State)

Where the vocabulary breaks down in practice

These are the failures that show up in real review threads, not hypotheticals. Each one is a case where the name did the opposite of its job — it obscured a disagreement instead of compressing one.

The name did not mean what the other person heard
TriggerSymptomCauseResponse
"Use a factory here"Author adds a static method; reviewer wanted an injected abstract factoryOne name covers four distinct shapes with different dependency implicationsSay what problem you are solving: "callers should not choose the implementation" (Factory).
"This should be a repository"Three engineers implement three different thingsThe name means one thing in the DDD book and another in Spring or Rails (What a Framework Charges)Define it in the team glossary once, and link the glossary in review (Ubiquitous Language).
"It is just the observer pattern"Discussion of traceability cost never happensA recognised name reads as a settled decisionNames describe; they do not justify. Ask what the next change costs either way (Local Reasoning).
"We follow SOLID and the GoF patterns"Interfaces with one implementation everywhereA catalogue treated as a checklist rather than a set of conditional responsesRequire a named observed problem before any abstraction lands (Premature Abstraction).
"This is a singleton"Tests interfere with each other and nobody knows whyThe name blesses global mutable stateOne instance is a wiring decision, not a class decision — construct one and pass it (Wiring and the Composition Root).

You already wrote one; now name it

This is the direction of travel that works. The code below was written by someone solving a problem, with no pattern in mind. It happens to be a strategy, and knowing that is worth exactly one thing: the next person can say "the pricing strategies" in review and be understood.

What the name does not do is argue for the next abstraction. The three rules exist because there were three rules; if there had been one, the correct code would have been an if, and it would not have needed a name at all (The Rule of Three).

Written without the book, named afterwards
1// Three rules existed. Someone wrote the obvious thing:
2type PricingRule = (order: Order) => Money
3
4const standard: PricingRule = (o) => o.subtotal
5const discount: PricingRule = (o) => o.subtotal.times(0.9)
6const premium: PricingRule = (o) => o.subtotal.minus(o.loyaltyCredit)
7
8const rules: Record<Tier, PricingRule> = { standard, discount, premium }
9
10export const priceFor = (o: Order) => rules[o.tier](o)
11
12// This is the Strategy pattern. In a language with function
13// values it is four lines and no classes — which is why the
14// name is worth knowing and the UML is not.

The pattern is present; the ceremony is absent. If someone "applies Strategy" to this and produces an interface plus three classes plus a factory, the pattern has been added and the design has not changed.

How to build it

Most important first.

  • Use patterns to describe code you already have. Recognising that three wrappers around a client are decorators is useful; deciding to add a decorator because you have heard of them is not (Decorator).
  • Always state the problem alongside the name. "Adapter, because we do not want vendor types past this line" survives a team change; "Adapter" alone does not (Adapter).
  • Prefer the plainest name available. PricingRule communicates more than PricingStrategy to everyone who has not read the book, and the same to everyone who has (Naming).
  • Learn the *problems* the catalogue documents — behaviour varying by kind, construction that has grown a decision, an interface you do not control — because those transfer even when the pattern does not.
  • Ask what the language already gives you. If the answer is "a function", use a function (KISS: Simplest for the Requirements You Have).
  • Treat the catalogue as history: a 1994 book about C++ and Smalltalk, written when the languages lacked closures, generics and modules, whose advice was already qualified by its authors and is quoted without the qualifications.

What the next change costs

The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.

Cost of the next change
  • Vocabulary changes no code, so it changes no change cost directly. What it changes is the cost of the *conversation* about a change — and on a six-person team that is a real, if unmeasurable, saving.
  • Where it does affect change cost, it does so badly: a pattern adopted because it had a name adds indirection that every subsequent change pays for.
  • The concrete test to apply before any pattern: name the next requirement, and say what it costs with and without. If the answer is the same, the pattern is decoration (The Cost of Change).
  • Team-glossary changes are cheap forever — a wiki edit — which is exactly what makes vocabulary a better investment than structure when the problem is that people are talking past each other.
What the recommended approach costs
  • A shared vocabulary makes discussion faster and makes bad ideas easier to propose, because a named bad idea sounds like an established one.
  • Insisting on plain names costs the compression the catalogue offers, and in a team that all read the same book that compression is genuine.
  • Teaching patterns at all creates the risk this lesson exists to counter. The alternative — not teaching them — leaves engineers unable to read half the codebases they will encounter.

What can go wrong

Failure modes
  • Cargo culting: the shape is copied without the constraint that made it necessary, producing a factory whose only job is to call a constructor (Factory).
  • The vocabulary becomes a shibboleth — knowing the names signals seniority, so people use them to signal rather than to communicate.
  • The mitigation fails too: banning pattern names produces long descriptive paragraphs in review where one word would have done, and the team loses the genuine compression.
  • Names get attached to shapes that are almost-but-not the pattern, so the name now actively misleads: everyone assumes an Observer where the call is synchronous and ordered (Observer).
Dependencies, and their direction
  • The vocabulary depends on shared reading. A name only compresses if both people decode it the same way, and that assumption is wrong more often than teams check.
  • Framework conventions override the catalogue in practice. In a Spring or Rails codebase, "service" and "repository" mean what the framework means, whatever the book says (What a Framework Charges).
  • Nothing in the code should depend on a pattern name. A class called AbstractOrderStrategyFactory has encoded a vocabulary decision into the type system, where it cannot be revised (Naming and Domain Language).
Misreads
  • "Patterns are bad." They are a vocabulary with a bad reputation earned by its users. Adapter and Command are load-bearing in codebases that have never heard of the book (Adapter).
  • "Patterns make code cleaner." They do not make code anything. Each one trades a specific rigidity for a specific indirection, and whether that is an improvement depends entirely on which change arrives next (Pattern Overuse).
  • "Modern languages made patterns obsolete." They retired the ones that were language workarounds. The ones about boundaries and about behaviour that genuinely varies at runtime are unaffected.
  • "Knowing the names means knowing the designs." The names are the least transferable part. What transfers is the ability to state the problem a shape responds to (The Design Loop).

Testing it, and how it ages

What to test, and at which boundary
  • There is nothing to test. That is worth stating, because it distinguishes a naming decision from a design decision and teams routinely conflate them.
  • What can be checked is the glossary: does a new engineer, given the team's definition, correctly identify the shapes in the codebase?
  • For any pattern actually adopted, test the behaviour it enables — a second implementation, a wrapped call, a replayed command — not the structure (Testing as Design Feedback).
How this design ages
  • The catalogue ages against languages, not against time. Every language feature added since 1994 — closures, generics, sum types, modules, async — retired part of it.
  • The names outlive the shapes, which is why "singleton" now means a module-level value that has none of the original pattern's lazy-initialisation machinery, and nobody minds.
  • What has aged well is the *problem statements*, and the parts of the catalogue that map onto boundaries rather than object graphs — Adapter and Facade are still doing work (Anti-Corruption Layer).

Where this applies

This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.

  • PARADIGM-SPECIFICThe 1994 catalogue documents object-oriented solutions in languages without first-class functions. In a functional language roughly half of it is a function, a partial application or a record of functions, and the remainder — Adapter, Facade, Observer — reappears with different names; a Clojure or Haskell engineer who has never heard of Strategy writes it constantly and correctly.
  • CONTESTEDThe strongest defence of the canon: it gave the profession its first shared design vocabulary, and the alternative to imperfect shared names is every team inventing private ones, which is measurably worse for onboarding and for reading unfamiliar code. The strongest attack, from Peter Norvig onward, is that most of the catalogue is invisible or trivial in a sufficiently expressive language, so what is being taught as design is largely a record of what C++ could not say in 1994. Both are right about different halves of the book.
  • SCALE-SPECIFICThe vocabulary pays where people who have never spoken read each other's code — a large codebase, a public library, an open-source contribution. On a four-person team that talks daily, a shared local glossary beats the canonical names, because it can be precise about this codebase rather than about codebases in general.

Where the depth lives

This domain teaches the codebase-level structure and hands the rest off.

Domains that do not exist yet
  • Programming Languages & Runtime Internals — which patterns a language feature retires: closures retire Strategy and Command, modules retire Singleton, generics retire much of Abstract Factory, and iterators retire Iterator. Reading the catalogue as a record of missing features is the most useful lens available on it.