The most useful design patterns, summarised
Open in the interactive app: practice, quiz, mock interview →In one minute
- Fifteen patterns cover most object-oriented design you will write or be asked about. Each has its own lesson in this course; this page is the map and the selection method.
- Pick a pattern from the shape of the problem, not from its name. "Behaviour varies by a value I choose at runtime" is Strategy; "behaviour varies by where the object is in its lifecycle" is State; "callers need the wrong interface" is Adapter.
- The fifteen reduce to four mechanisms: an interface with several implementations, a wrapper, a table from key to behaviour, and a list of callbacks. The mechanism tells you the runtime cost.
- A pattern needs two real variants or a scheduled change. Otherwise the plain function is the better design.
- The fastest test in review: say the problem in one sentence without the pattern's name. If you cannot, the pattern is decoration.
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.
| Pattern | Problem shape | Smallest honest form | Lesson |
|---|---|---|---|
| Strategy | One step has interchangeable algorithms the caller picks | A function parameter or a map of functions | Strategy |
| Observer | Many parties react to a change the source should not know about | A list of callbacks with unsubscribe | Observer |
| Factory Method | The concrete type depends on runtime input | A constructor function or a registry keyed by name | Factory Method |
| Builder | Many optional parts, valid only when complete | Named or keyword arguments, or a builder with build() that validates | Builder |
| Adapter | Code needs interface A, the library offers interface B | A thin wrapper that translates calls | Adapter |
| Decorator | Add retry, cache or metrics without editing the core | A wrapper with the same interface as what it wraps | Decorator |
| Facade | Callers keep misusing a many-step subsystem | One function or class that does the safe sequence | Facade |
| Proxy | Gate access: lazy load, remote, permission, rate limit | A stand-in with the same interface that decides whether to forward | Proxy |
| Composite | Parts and wholes treated alike (folders, UI trees) | A node type whose children are the same type | Composite |
| Command | Actions must be queued, logged, retried or undone | A record of "what to do" plus execute and undo | Command |
| State | Behaviour depends on lifecycle stage | An enum plus a transition table, or one class per state | State |
| Template Method | Fixed sequence, some steps vary | A function that takes the varying steps as parameters | Template Method |
| Iterator | Walk a collection without knowing its storage | A generator or Next() method | Iterator |
| Chain of Responsibility | Ordered handlers, any may answer or stop the request | A slice of handlers or middleware | Chain of Responsibility |
| Singleton | Exactly one instance per process | One instance created in main and injected | Singleton |
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.
- 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).
- 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.
- Table from key to behaviour. Factory registries, Strategy maps, State transition tables and Command dispatchers are dictionaries: a switch statement turned into data.
- 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.
- 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.
- 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.
- 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.
- 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
- One implementation and no change in sight. YAGNI applies. Adding the interface later is a mechanical refactor that takes minutes with a type checker and tests.
- The language already does it. A function value, a generator or
functools.lru_cacheis the pattern; Intro to Design Patterns explains why. - The problem is data, not behaviour. If variants differ only in numbers (rates, thresholds, timeouts), a table of values beats a class per variant.
- Nobody can name the change it protects against. A pattern is a bet on what will vary; without a named change the bet is random.
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
- Requirements change in predictable places (a new payment method, a new channel, a new state), so designers isolate those places behind a seam.
- A seam must let new code plug in without editing old code, which in typed languages means an interface or a function type.
- Indirection costs a pointer load, a lost inline and sometimes an allocation, so seams are worth it only where change is likely.
- The same few seams kept recurring across projects, so they got names, and the names let teams agree on a design in one word.
- 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.
- Creating objects: the channel is chosen per customer, and SMS is scheduled, so a small factory keyed by channel name is justified. Builder is not: a reminder has four fields.
- Interfaces: the SMS SDK takes a phone number and template id, not a
Message, so it gets an Adapter. Retry and audit are cross-cutting and identical across channels, so they are two Decorators around the channel interface. - Varying behaviour: whether to send depends on the invoice's lifecycle stage, so State, in practice an enum plus a transition table, since the stage is already a database column. The 3, 7, 14 schedule is data in config, not a Strategy.
- Travelling events: "invoice paid" must cancel pending reminders. The payments module already publishes events, so the reminder module subscribes: Observer with no new infrastructure.
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
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
Plain function or switch | Readable; compiler checks exhaustiveness; inlinable | Every new variant edits the same function | One or two variants, owned by one team |
| Map of functions | Variants added as data; one line per variant | Runtime errors for unknown keys; no per-variant state | Variants are stateless and chosen by key |
| Interface with classes | Variants carry state and several methods; testable in isolation | More files; indirect calls; harder to read top-down | Variants have state or several related operations |
| Wrapper stack (Decorator, Proxy) | Cross-cutting concerns stay out of core code | Order of layers matters and is easy to get wrong; allocations | Retries, caching, metrics, auth around a stable interface |
| No seam yet, refactor later | Zero indirection today | Needs tests and a type checker to make the later change safe | No second variant and no named change |
Pitfalls in production
- Pattern by name, not by problem. A codebase full of
*Factory,*Managerand*Strategyclasses each with one implementation. Fix: inline them; keep a seam only where two variants exist or a change is scheduled. - Strategy where State was needed. The caller sets the "strategy" for an order's stage and forgets to change it on transition, so a shipped order still accepts edits. Fix: let the object own its stage and expose transitions, not setters.
- Decorator order bugs. Cache outside Retry caches the error; Retry outside Auth retries a 401 three times. Fix: build the stack in one function with a comment on the order, and test the order.
- Observer leaks. Subscribers added per request and never removed; memory grows until restart. Fix: return an unsubscribe handle and tie it to the subscriber's lifetime.
- Singleton as hidden global. Tests pass alone and fail together because state leaks across them. Fix: construct once in the composition root and pass it in.
Leadership lens
- Make "which requirement does this seam serve?" a standard review question. A pattern without an answer is cost without benefit, and it compounds as new hires copy it.
- Agree on a small house vocabulary. Teams that use Strategy, State, Decorator and Observer consistently review designs faster than teams that know thirty names loosely.
- Tell coding agents the smallest form you want ("a map from channel name to a send function, no classes") instead of "make it extensible". Agents follow concrete shapes well and over-build vague ones.
- Budget refactors as the variant count grows. The right moment to add a Strategy is the third
elif, and that work is cheap if someone owns it.
Check yourself
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.
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).
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.
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
- 10 Design Patterns Explained in 10 Minutes (Fireship): a quick tour of the most common patterns with the "do you need it?" caveat for each.
Free resources
- Refactoring.Guru design pattern catalog: one page per pattern with intent, structure and applicability, useful as a lookup next to the table above.
- Design Patterns in Dynamic Languages (Peter Norvig): shows that 16 of the 23 GoF patterns become simpler or invisible with first-class functions and dynamic types.
- python-patterns.guide (Brandon Rhodes): which patterns still earn their place in Python, and which the language absorbed.
- Game Programming Patterns (Robert Nystrom): a free book that revisits Command, Observer, State, Flyweight and others with performance in mind.
- Beck Design Rules (Martin Fowler): four rules of simple design, the check to apply before any pattern.