System Design · Architecture

Design Patterns

The named solutions worth knowing, and the discipline of not applying them until the problem shows up.

— min read System Design

A Vocabulary, Not A Checklist

A design pattern is a named solution to a recurring problem. Its value is mostly communication: saying "put an adapter round it" conveys a structure in three words that would otherwise take a paragraph.

The failure mode is treating the catalogue as a list of things to use. Patterns are recognised, not applied — you notice you have written one, or you reach for one because the problem it solves has actually appeared.

A codebase with a factory producing one product, an interface with one implementation and a strategy with one strategy has not been designed. It has been decorated, and every layer is indirection a reader must walk through to find the code that does something.

Creational Patterns

These control how objects get made — useful when construction itself is the complicated part.

PatternSolvesReach for it when
FactoryChoosing an implementation at runtimeThe choice depends on configuration or input
BuilderConstructors with many optional parametersYou are counting positional arguments
SingletonOne shared instanceRarely — it is global state with a nicer name
Dependency injectionHanding collaborators in rather than constructing themAlmost always; it is what makes testing possible
Singleton is the most overused pattern in the catalogue. A single instance is usually a lifetime concern the container should own, and a hand-rolled singleton is a global variable that also fights your tests.

Structural Patterns

These arrange objects into larger structures without rewriting them.

PatternSolves
AdapterAn interface you need does not match the one you have
FacadeA subsystem is more complicated than callers need
DecoratorAdding behaviour — caching, retries, logging — without touching the original
ProxyControlling access: lazy loading, permissions, remote calls
CompositeTreating a tree of things the same as a single thing
// decorator: retries wrapped round a client, which knows nothing about them
class RetryingClient {
  constructor(inner, attempts = 3) { this.inner = inner; this.attempts = attempts; }

  async fetch(id) {
    for (let i = 1; i <= this.attempts; i++) {
      try { return await this.inner.fetch(id); }
      catch (err) { if (i === this.attempts || !isTransient(err)) throw err; }
    }
  }
}
Adapter and facade are the two that pay for themselves in almost every codebase — one contains a dependency you do not control, the other contains complexity you do.

Behavioural Patterns

PatternSolves
StrategySwapping an algorithm at runtime — pricing rules, sorting, retry policy
ObserverNotifying interested parties without coupling to them
StateBehaviour that changes with a lifecycle — order, subscription, document
CommandTurning a request into an object you can queue, log or undo
Template methodA fixed sequence with steps subclasses fill in

Strategy and state are the two that most often replace a growing conditional. A function branching on a type code across six methods is usually a strategy or a state machine that has not been written down.

Observer is easy to introduce and hard to debug: control flow disappears into subscribers and stack traces stop explaining anything. Worth it for genuine fan-out, expensive for a single listener.

Knowing When Not To

SignalReading
The same conditional appears in three placesA strategy or polymorphism is hiding there
A class changes for unrelated reasonsIt has two responsibilities
A test needs six mocks to constructToo many collaborators — split it
A pattern with one implementationSpeculative — delete the abstraction
You cannot name the problem it solvesDo not add it
The useful rule is to write the direct version first, and let duplication and change pressure tell you where an abstraction belongs. A pattern introduced before the third occurrence is a guess about the future, and those are usually wrong.

Interview Questions

What is a design pattern actually for?

Communication first: a shared name for a recurring structure. Its value is that a reviewer understands the shape immediately, not that using more of them makes code better.

Why is singleton criticised?

It is global state with a nicer name — hidden dependencies, awkward lifetimes and tests that interfere with each other. A container-managed single instance achieves the same without the coupling.

When would you use a decorator?

To add cross-cutting behaviour — retries, caching, logging, metrics — round an existing implementation without modifying it or its callers.

Strategy or a conditional?

A conditional is fine until the same branching appears in several places or new cases arrive regularly. At that point strategy makes each case a separate testable unit.

What is the cost of a premature abstraction?

Indirection with no payoff: an interface with one implementation is a layer every reader walks through, and it usually turns out to be the wrong shape when the second case finally arrives.

How do you decide an abstraction is justified?

By pressure from real duplication and real change, not anticipation. Write the direct version, and let the third occurrence tell you what the abstraction should be.

Quick Quiz

1. The primary value of a design pattern is…
2. A factory producing one product is…
3. Adding retries around a client without changing it is a…
4. A conditional repeated across several methods suggests…
5. You should introduce an abstraction when…