Engineering Atlas › Low-Level Design

The most useful design patterns, summarised

15 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

The fifteen and the problem each one solves

The "problem shape" column is the sentence you should hear in a requirement before reaching for the pattern; the last column names the lesson that goes deep on it.

PatternProblem shapeSmallest honest formLesson
StrategyOne step has interchangeable algorithms the caller picksA function parameter or a map of functionsStrategy
ObserverMany parties react to a change the source should not know aboutA list of callbacks with unsubscribeObserver
Factory MethodThe concrete type depends on runtime inputA constructor function or a registry keyed by nameFactory Method
BuilderMany optional parts, valid only when completeNamed or keyword arguments, or a builder with build() that validatesBuilder
AdapterCode needs interface A, the library offers interface BA thin wrapper that translates callsAdapter
DecoratorAdd retry, cache or metrics without editing the coreA wrapper with the same interface as what it wrapsDecorator
FacadeCallers keep misusing a many-step subsystemOne function or class that does the safe sequenceFacade
ProxyGate access: lazy load, remote, permission, rate limitA stand-in with the same interface that decides whether to forwardProxy
CompositeParts and wholes treated alike (folders, UI trees)A node type whose children are the same typeComposite
CommandActions must be queued, logged, retried or undoneA record of "what to do" plus execute and undoCommand
StateBehaviour depends on lifecycle stageAn enum plus a transition table, or one class per stateState
Template MethodFixed sequence, some steps varyA function that takes the varying steps as parametersTemplate Method
IteratorWalk a collection without knowing its storageA generator or Next() methodIterator
Chain of ResponsibilityOrdered handlers, any may answer or stop the requestA slice of handlers or middlewareChain of Responsibility
SingletonExactly one instance per processOne instance created in main and injectedSingleton

The remaining patterns also have lessons here: Abstract Factory (matching families of products), Prototype (cloning a configured template), Bridge (two dimensions that vary independently), Flyweight (millions of objects sharing immutable state), Mediator (widgets that would otherwise all reference each other), Memento (snapshots for undo), Visitor (new operations over a closed tree of types) and Null Object (absence as a harmless default). Intro to Design Patterns covers where the catalogue came from and why many entries became language features.

Four mechanisms underneath

The mechanics repeat across the fifteen.

  1. Interface plus implementations. Strategy, State, Factory Method, Template Method, Iterator and Command put varying behaviour behind one interface. What differs is who picks the implementation: the caller (Strategy), the object as its stage changes (State), a lookup on input (Factory Method), a subclass (Template Method).
  2. Wrapper with the same interface. Decorator always forwards and adds behaviour, Proxy decides whether to forward, Adapter changes the interface shape, Composite wraps many children.
  3. Table from key to behaviour. Factory registries, Strategy maps, State transition tables and Command dispatchers are dictionaries: a switch statement turned into data.
  4. List of callbacks. Observer calls every function in its list; a Chain calls them in order until one handles the request.

Seeing the mechanism ends arguments about names. Instead of "is this Strategy or State?", ask "who changes the implementation, and is the change a lifecycle transition?"

Picking one quickly

Ask four questions in order, and stop at the first yes.

  1. Is something being created, and is the choice or the construction hard? Choice by input points to Factory Method; many optional fields point to Builder; matching families point to Abstract Factory.
  2. Is an interface wrong, too big or missing a capability? The wrong shape points to Adapter, too many steps to Facade, a missing cross-cutting behaviour to Decorator, gated access to Proxy.
  3. Does behaviour vary? By caller choice it is Strategy; by lifecycle stage it is State; by one step in a fixed sequence it is Template Method; by node type in a tree, Composite or Visitor.
  4. Do events or requests need to travel? Fan-out to many listeners is Observer; ordered handlers that may stop the request are a Chain; actions that must be stored, replayed or undone are Commands.

Then count variants. A Strategy with one strategy or a Factory that builds one type is indirection with nothing on the other side; write the direct call instead.

When not to use any pattern

Reviewing agent-written code

Ask an agent to "make the pricing extensible" and you may get a PricingStrategyFactory, an abstract BasePricingStrategy and a PricingContext, for a codebase with one pricing rule. In review, apply the one-sentence test, count the variants, and ask for the smallest honest form from the table. The reverse also happens: an agent adds a fourth elif to a value that is already switched on in three other functions. That repeated switch is the signal a Strategy map or State table is now worth it.

The same shape elsewhere

Choosing a pattern from problem flags is a classification problem, the same one a doctor's differential diagnosis or an incident triage runbook solves: observe a few features, rule out candidates whose preconditions fail, prefer the most specific match. The practice task builds exactly that recommender, and its "most specific rule wins" step is the same conflict resolution that CSS selector specificity and longest-prefix IP routing use.

Down the stack

Layer 1The contract

Strategy and State promise substitutability: any implementation can replace another without the caller changing (the Liskov rule). Decorator and Proxy promise the same interface as the wrapped object, so callers cannot tell how many layers there are. Observer promises every live subscriber is called once per event, and an unsubscribed one is not. Command promises undo(execute(s)) == s. Builder promises build() never returns an invalid object. Iterator promises every element once, in a defined order, and a defined behaviour when the collection changes underneath. Singleton promises one instance per process, the promise most often broken by tests and forks.

Layer 2The mechanism

An interface call in C++ and Java goes through a vtable: the object header points at a per-class table of function pointers, and the call loads one and jumps. Go stores an interface as two words, a pointer to an itab (type plus method pointers) and a pointer to the data, so a Strategy held in an interface field costs 16 bytes and one indirect call. JavaScript has no vtables: strategy.apply(x) walks the prototype chain, and V8 caches the result per call site in an inline cache keyed by the object's hidden class. A table of behaviours is a hash map: Python dict, Go map (buckets of 8 entries, or Swiss tables since Go 1.24), JS Map. A list of callbacks is an array of closures, each a function pointer plus captured variables.

Layer 3The machine

A well-predicted indirect call costs about 1 to 2 ns; a mispredicted one about 15 to 20 cycles (5 ns at 3 to 4 GHz), which matters only when one hot call site sees many implementations. The larger cost is lost inlining: the compiler cannot inline through an interface it cannot see through, so a tiny helper that could have been vectorised stays a call. Wrappers cost allocation and pointer chasing: a five-layer Decorator stack in Go is five heap objects, and each call follows five pointers that may each miss L1 cache (about 1 ns hit, 4 ns L2, 80 to 100 ns main memory). Command objects allocate one record per action; an undo history of 100,000 edits at 64 bytes each is 6.4 MB. Observer under concurrency needs a lock or a copy-on-write subscriber list; Singleton needs sync.Once in Go or a module-level instance in Python, whose import lock already serialises initialisation.

Why it's built this way

  1. Requirements change in predictable places (a new payment method, a new channel, a new state), so designers isolate those places behind a seam.
  2. A seam must let new code plug in without editing old code, which in typed languages means an interface or a function type.
  3. Indirection costs a pointer load, a lost inline and sometimes an allocation, so seams are worth it only where change is likely.
  4. The same few seams kept recurring across projects, so they got names, and the names let teams agree on a design in one word.
  5. Names became fashion, which is why the selection discipline (problem shape first, two variants, one-sentence test) matters more than the catalogue.

Diagram

flowchart TD
  Q["What is the problem?"] --> C{"Creating objects is hard?"}
  C -- "type from input" --> FM["Factory Method"]
  C -- "many options" --> BU["Builder"]
  C -- "no" --> I{"Interface is the problem?"}
  I -- "wrong shape" --> AD["Adapter"]
  I -- "too many steps" --> FA["Facade"]
  I -- "add behaviour" --> DE["Decorator"]
  I -- "gate access" --> PR["Proxy"]
  I -- "no" --> V{"Behaviour varies?"}
  V -- "caller picks" --> ST["Strategy"]
  V -- "lifecycle stage" --> SA["State"]
  V -- "one step of fixed flow" --> TM["Template Method"]
  V -- "no" --> E{"Events or requests travel?"}
  E -- "fan-out" --> OB["Observer"]
  E -- "ordered handlers" --> CR["Chain of Responsibility"]
  E -- "store or undo" --> CO["Command"]
  E -- "no" --> N["Plain code"]

Worked example

A team is adding invoice reminders to a billing service: send reminders 3, 7 and 14 days after the due date; email today, SMS next quarter; retry provider timeouts; audit every send for finance; stop once an invoice is paid, disputed or written off. Volume is about 40,000 reminders a day. Walk the four questions.

Rejected: a ReminderStrategy hierarchy (one way to remind), a Singleton scheduler (create one in main and inject it), and a Command per reminder (the job table already records what to send).

Cost check: each send passes Retry, Audit and the adapter, three interface calls costing nanoseconds against a 150 ms provider round trip, at under one send per second on average. Performance does not constrain this design; readability does, so there are four seams, each tied to a named requirement.

Trade-offs

OptionStrengthsWeaknessesChoose when
Plain function or switchReadable; compiler checks exhaustiveness; inlinableEvery new variant edits the same functionOne or two variants, owned by one team
Map of functionsVariants added as data; one line per variantRuntime errors for unknown keys; no per-variant stateVariants are stateless and chosen by key
Interface with classesVariants carry state and several methods; testable in isolationMore files; indirect calls; harder to read top-downVariants have state or several related operations
Wrapper stack (Decorator, Proxy)Cross-cutting concerns stay out of core codeOrder of layers matters and is easy to get wrong; allocationsRetries, caching, metrics, auth around a stable interface
No seam yet, refactor laterZero indirection todayNeeds tests and a type checker to make the later change safeNo second variant and no named change

Pitfalls in production

Leadership lens

Check yourself

  1. An order's behaviour differs between draft, submitted and shipped, and someone proposes Strategy. What is the better fit, and why?

    Show answer

    State, because the variation follows the object's lifecycle and the object itself must change implementation on transitions; Strategy is for variation the caller chooses.

  2. Decorator and Proxy have the same structure. What tells them apart?

    Show answer

    Intent: a Decorator always forwards and adds behaviour around the call, while a Proxy decides whether and when to forward (lazy loading, access checks, remote calls).

  3. Why do Strategy, Factory registries and State tables often end up as dictionaries in Python or Go?

    Show answer

    Because all three map a key to a behaviour, and when the behaviour is stateless a map of functions gives the same extension point with less code than a class per variant.

  4. A teammate adds an interface with one implementation "for testability". When is that justified?

    Show answer

    When the real implementation does I/O or is slow and tests need a fake; then the fake is the second implementation. If the real one is pure and fast, test it directly.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →