What design patterns are for
Open in the interactive app: practice, quiz, mock interview →In one minute
- A design pattern is a named, reusable answer to a problem that keeps recurring in a given context, written down with its forces and consequences. It is a description you adapt, not code you import.
- The idea came from architecture: Christopher Alexander's A Pattern Language (1977) catalogued 253 patterns for towns and buildings. Kent Beck and Ward Cunningham applied it to Smalltalk in 1987, and the "Gang of Four" book (Gamma, Helm, Johnson, Vlissides, 1994) gave software its standard catalogue of 23.
- The GoF sort patterns by purpose: creational (how objects are made), structural (how objects are composed) and behavioural (how objects divide work and communicate).
- Many patterns compensate for missing language features. First-class functions absorb Strategy and Command, generators absorb Iterator, generics remove a family of casting wrappers, and sum types with pattern matching replace most Visitors.
- The lasting value is the shared vocabulary and the two principles under the catalogue: program to an interface, and prefer composition to inheritance.
How it works
What a pattern is, and what it is not
A pattern has four parts: a name short enough to use in conversation, a problem with the context in which it appears, a solution described as roles and relationships rather than specific classes, and consequences, meaning what it costs and what it makes harder. The consequences are the part most people skip and the part that matters most. "Observer decouples the subject from its listeners" is half the story; the other half is "and now the order of notifications, errors thrown by listeners and memory held by forgotten subscriptions are your problem."
A pattern is not a library. You cannot import Observer, because the solution has to be fitted to your types, threading model and error handling. It is also not an algorithm: quicksort has one correct implementation shape, while two correct Decorators can look quite different. And it is not a goal. A pattern solves a problem that a design has; it does not improve a design that lacks the problem.
Where the idea came from
Alexander, an architect, noticed that good buildings reuse the same solutions to the same tensions, such as a seat by a window where people want light. A Pattern Language named 253 of them, larger patterns setting the context for smaller ones, so that non-experts could take part in design.
Beck and Cunningham's 1987 OOPSLA paper, "Using Pattern Languages for Object-Oriented Programs", borrowed the idea for Smalltalk user interfaces and reported that a handful of patterns let newcomers design usable windows. Through the early 1990s, the Hillside Group and the first Pattern Languages of Programs conference (PLoP, 1994) built a community around writing patterns. Ward Cunningham created the first wiki in 1995 to host the Portland Pattern Repository, so the wiki itself is a by-product of the patterns movement.
The GoF book, Design Patterns: Elements of Reusable Object-Oriented Software, appeared in late 1994 with examples in C++ and Smalltalk. Each of its 23 entries follows one template, from intent and applicability through structure and consequences to sample code and known uses. The known-uses section mattered: each pattern was shown in existing systems. Patterns were discovered, not invented.
The three families
Creational patterns separate what gets created from who asks for it: Abstract Factory, Builder, Factory Method, Prototype, Singleton. They exist because a constructor call names a concrete class, and naming it everywhere spreads that decision through the code.
Structural patterns arrange objects into larger structures while keeping interfaces simple: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy. Most of them wrap one object in another that has the same or a translated interface.
Behavioural patterns distribute responsibility and communication: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor. Most of them turn a piece of behaviour, a request or a traversal into an object that can be passed around.
The GoF also split patterns by scope. Class patterns (Factory Method, Template Method, the class form of Adapter) fix relationships at compile time through inheritance; object patterns fix them at runtime through composition. Most of the catalogue is object-scoped, which reflects the book's second principle.
The two principles under the catalogue
The book's opening chapter is more durable than its catalogue. It argues for programming to an interface, not an implementation, so that callers depend on what an object does rather than what it is, and for favouring object composition over class inheritance, because inheritance exposes a subclass to its parent's internals and fixes the relationship at compile time. Nearly every pattern is one of these principles applied to a specific problem. If you remember only the principles, you can rederive most patterns when you need them. The SOLID lessons in this course (OCP and DIP in particular) are later restatements of the same ideas.
Patterns in modern languages
In 1996 Peter Norvig showed that 16 of the 23 GoF patterns are simpler or invisible in a language with first-class functions, dynamic types and macros. The trend since then has been for mainstream languages to absorb patterns as features.
| Pattern | What absorbed it | Example |
|---|---|---|
| Strategy, Command | First-class functions and closures | sorted(rows, key=by_price); Java 8 lambdas (2014) |
| Template Method | Higher-order functions | Pass the varying step as a function argument |
| Iterator | Generators and language-level iteration | Python generators (PEP 255, 2001); C# yield return (2005); Go range-over-func (1.23, 2024) |
| Visitor | Sum types and pattern matching | Rust enum with match; TypeScript discriminated unions; Java sealed classes plus switch patterns (Java 21) |
| Builder | Named and default arguments | Python keyword arguments; Kotlin named parameters |
| Singleton | Modules | A Python module or a Go package-level value is loaded once |
| Prototype | Prototype-based objects | JavaScript Object.create(proto) |
| Observer | Event and reactive libraries | DOM addEventListener, Node EventEmitter, RxJS |
| Decorator (function form) | Syntax for wrapping functions | Python @decorator (PEP 318, 2004) |
Generics remove another category: the casting wrappers around containers that pre-generics Java and Go needed. Before Go 1.18 (2022) a generic list held interface{} and every caller cast back.
What remains after the language takes its share is the design decision. A lambda makes Strategy cheap to write, but someone still decides where the seam goes and what the function's contract is. Pattern matching makes Visitor unnecessary for closed type sets, but the GoF Visitor still wins when other teams must add operations without editing your types. The patterns that survive are mostly about structure between components (Adapter, Facade, Proxy, Composite, State, Mediator) rather than about working around the language.
Criticism worth knowing
Paul Graham argued in 2002 that patterns in a program are a sign that the programmer is doing by hand what a more powerful language would do automatically. That is partly right, as the table shows. The other common complaint is cargo culting: patterns applied because they exist, producing AbstractSingletonProxyFactoryBean-style layering with no problem underneath. Both critiques attack misuse rather than the idea. The idea, a named solution with stated consequences, is how teams share design knowledge, and it now extends well past the GoF: Fowler's enterprise application patterns, the microservices and cloud patterns, concurrency patterns, and anti-patterns that name what not to do.
Learning patterns with an AI assistant
An assistant writes a textbook Observer in seconds, so the skill is no longer the class diagram. It is recognising the problem, choosing the smallest form your language allows, and spotting generated code that applies a 1994 C++ shape where a one-line feature exists. The Design Patterns lesson gives the selection method and the full catalogue table.
The same shape elsewhere
"Name a recurring solution, state its trade-offs, let people compose them" appears far from code. Military doctrine, clinical guidelines, legal precedents and SRE runbooks are pattern languages: each entry has a context, a recommended action and known consequences, and practitioners combine them. Distributed systems have their own catalogue (Saga, Outbox, Circuit Breaker, Bulkhead), covered in the system design course.
Down the stack
Layer 1The contract
A pattern's written form is its contract with the reader. Intent says which problem it solves; applicability says when to use it; consequences say what you pay. When a code reviewer says "this is a Decorator", the team can assume the wrapper has the same interface as what it wraps and always forwards; when it is a Proxy, forwarding may be skipped. The name carries those guarantees, which is why misnaming a class (calling a Proxy a Decorator) causes real bugs: readers trust the wrong contract.
Layer 2The mechanism
When a language absorbs a pattern, the compiler or runtime implements the pattern's structure for you. A closure in Go or JavaScript is a function pointer plus an environment record holding captured variables, which is exactly the GoF Command object with its receiver fields, generated by the compiler. A Python generator is an object that owns a suspended frame: next() resumes the frame at the last yield, and the frame's local variables are the iterator's state, so the explicit Iterator class with a cursor field disappears. Go's range-over-func rewrites for x := range seq { body } into a call seq(yield) where yield is a compiler-generated closure containing the loop body that returns false on break. A Rust match on an enum compiles to a check of the discriminant tag followed by a jump table or branch chain, with exhaustiveness checked at compile time, replacing Visitor's double dispatch.
Layer 3The machine
Absorbed patterns usually get cheaper, because the compiler knows more. A Go closure that does not escape stays on the stack and costs no allocation; one that escapes allocates its environment on the heap once, typically a few dozen bytes. Range-over-func loop bodies can often be inlined so the iterator costs about the same as a hand-written loop. A Visitor costs two indirect calls per node (accept, then visit), each a vtable or itab lookup of about 1 to 2 ns when predicted; a match on a tag is one compare and a predictable branch, often under 1 ns, and the tag sits in the same cache line as the data. A Python generator, by contrast, allocates a generator object and frame (a few hundred bytes), and each next() resumes interpreted bytecode, so a list comprehension over a small list can beat a generator pipeline.
Why it's built this way
- Software design keeps meeting the same tensions (creation versus use, interface versus implementation, fixed flow versus varying step).
- Solving them from scratch each time is slow and inconsistent, so practitioners named the recurring solutions and wrote down their costs.
- In 1994 the mainstream languages (C++, early Java) lacked closures, generics and pattern matching, so the solutions had to be built from classes and interfaces.
- Language designers saw the same boilerplate everywhere and added features that generate it, so many patterns shrank into syntax.
- The decisions those features cannot make (where to put a seam, who owns a lifecycle) are what pattern knowledge is for today.
Diagram
sequenceDiagram participant L as "for x := range seq" participant S as "seq(yield)" participant Y as "yield (loop body)" L->>S: call seq with compiler-made yield S->>Y: yield(1) Y-->>S: true (keep going) S->>Y: yield(2) Y-->>S: false (body hit break) S-->>L: seq returns, loop ends
Worked example
A payments team inherits a 2014 Java service. FeeCalculator has a 180-line switch with 11 fee types, and near-copies live in FeePreview and FeeAuditReport. Twice in a year a new fee type was added to only two of the three, so previews disagreed with charges.
The textbook answer is Strategy: an interface FeeRule and 11 classes. The team, now on Java 21, chose the smaller form the language allows: a Map<FeeType, FeeFunction> where FeeFunction is a functional interface, populated with lambdas and method references in one registry class. The three call sites became registry.fee(type, amount, qty). The refactor was guarded by a characterisation test: before touching anything, they generated 4,000 input combinations (11 types times a grid of amounts and quantities, plus invalid inputs) and recorded the old outputs and error messages as a golden file. The new code had to match all 4,000.
The test caught two things. The old code validated amounts before looking at the type, and the first registry draft did the reverse, changing an error message a client parsed. And one old case fell through into the next (a missing break), producing a fee finance had relied on for years; the team kept it, made it explicit, and filed a ticket for finance to decide.
The result: 11 lambdas of 3 to 12 lines each, one registry, no class hierarchy. Adding a fee type is one line, and a startup check fails if the EnumMap lacks an entry for any FeeType. Lookups add one hash probe and one interface call per fee, a few nanoseconds against a request that spends 30 ms in the database.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Classic GoF class structure | Explicit roles; each variant can hold state and several methods; familiar to Java and C++ readers | Many small classes; boilerplate in languages with lighter features | Variants carry state or several operations; team shares the vocabulary |
Language feature (lambda, generator, match) | Short; compiler-optimised; idiomatic | Less discoverable as a "pattern"; can hide a missing seam | The language offers it and variants are simple |
| Map or table of behaviours | Variants are data; easy to list and validate | Runtime errors for missing keys; harder to navigate in an IDE | Many stateless variants chosen by key |
| No pattern | Least code | Every change edits existing code | One variant and no known change |
Pitfalls in production
- Porting 1994 shapes into modern languages. A
StrategyInterface,ConcreteStrategyAandContextclass in Python for what is one function argument. Fix: ask what the language offers first, then write the smallest form. - Refactoring to a pattern without a characterisation test. Behaviour drifts in edge cases (error messages, check order, fall-through bugs that clients rely on). Fix: record current outputs for a broad input grid before the refactor and require an exact match.
- Losing exhaustiveness. Moving from an enum
switchto a string-keyed map removes the compiler's "missing case" check. Fix: key the map by the enum and assert at startup that every enum value has an entry. - Pattern names that lie. A class called
*Decoratorthat sometimes skips the wrapped call, or a*Factorythat caches. Fix: name by behaviour, or document the deviation at the type. - Using patterns as a seniority signal. Engineers add layers to look thorough, and reviewers hesitate to question them. Fix: in review, ask for the problem sentence before discussing the structure.
Leadership lens
- Treat the pattern vocabulary as a team tool: agree on the dozen names you use, and use them precisely in reviews and design docs.
- Set a house style for "which form": for example, functions and maps first, classes only when variants carry state. Write it down so humans and coding agents converge on it.
- When migrating language versions (Java 8 to 21, Go to 1.23), budget time to collapse old pattern boilerplate into features, guarded by characterisation tests. It cuts code and onboarding time.
- Hire and promote for judgement about when not to use a pattern, not for reciting the catalogue. Interview questions should present a problem, not a pattern name.
Check yourself
Why did the GoF require "known uses" before writing a pattern down?
Show answer
Because a pattern is a recurring solution observed in real systems, not a new invention; the known uses show it actually solves the problem in practice.
Which two principles underlie most of the GoF catalogue?
Show answer
Program to an interface, not an implementation; and favour object composition over class inheritance.
Go 1.23 added range-over-func. Which pattern does it absorb, and what does the compiler generate?
Show answer
Iterator; the compiler turns the loop body into a
yieldclosure passed to the sequence function, returningfalsewhen the body breaks.Rust and Java 21 have exhaustive pattern matching. When would you still use a GoF Visitor?
Show answer
When the set of types is open to extension by others or operations must be added by code you do not own;
matchsuits a closed set of types that you control.
Watch
- Stop Writing Classes (Jack Diederich, PyCon 2012): a funny, practical case for replacing class-based patterns with functions and built-ins.
- Design Patterns in Plain English (Programming with Mosh): a beginner-friendly walk through the GoF families and why they exist.
Free resources
- Design Patterns (Wikipedia): the GoF book's history, its 23 patterns by family, and the major criticisms.
- A Pattern Language (Wikipedia): Alexander's original book and how its structure inspired software patterns.
- Using Pattern Languages for Object-Oriented Programs (Beck and Cunningham, 1987): the short paper that brought patterns into software.
- Revenge of the Nerds (Paul Graham): the argument that patterns signal missing language power.
- Range Over Function Types (Go blog): how Go 1.23 turned Iterator into a language feature.
- JEP 441: Pattern Matching for switch (OpenJDK): the Java feature that replaces most Visitors over sealed hierarchies.