Engineering Atlas › Low-Level Design

What design patterns are for

15 min read · last reviewed 2026-10-17

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

In one minute

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.

PatternWhat absorbed itExample
Strategy, CommandFirst-class functions and closuressorted(rows, key=by_price); Java 8 lambdas (2014)
Template MethodHigher-order functionsPass the varying step as a function argument
IteratorGenerators and language-level iterationPython generators (PEP 255, 2001); C# yield return (2005); Go range-over-func (1.23, 2024)
VisitorSum types and pattern matchingRust enum with match; TypeScript discriminated unions; Java sealed classes plus switch patterns (Java 21)
BuilderNamed and default argumentsPython keyword arguments; Kotlin named parameters
SingletonModulesA Python module or a Go package-level value is loaded once
PrototypePrototype-based objectsJavaScript Object.create(proto)
ObserverEvent and reactive librariesDOM addEventListener, Node EventEmitter, RxJS
Decorator (function form)Syntax for wrapping functionsPython @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

  1. Software design keeps meeting the same tensions (creation versus use, interface versus implementation, fixed flow versus varying step).
  2. Solving them from scratch each time is slow and inconsistent, so practitioners named the recurring solutions and wrote down their costs.
  3. 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.
  4. Language designers saw the same boilerplate everywhere and added features that generate it, so many patterns shrank into syntax.
  5. 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

OptionStrengthsWeaknessesChoose when
Classic GoF class structureExplicit roles; each variant can hold state and several methods; familiar to Java and C++ readersMany small classes; boilerplate in languages with lighter featuresVariants carry state or several operations; team shares the vocabulary
Language feature (lambda, generator, match)Short; compiler-optimised; idiomaticLess discoverable as a "pattern"; can hide a missing seamThe language offers it and variants are simple
Map or table of behavioursVariants are data; easy to list and validateRuntime errors for missing keys; harder to navigate in an IDEMany stateless variants chosen by key
No patternLeast codeEvery change edits existing codeOne variant and no known change

Pitfalls in production

Leadership lens

Check yourself

  1. 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.

  2. 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.

  3. 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 yield closure passed to the sequence function, returning false when the body breaks.

  4. 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; match suits a closed set of types that you control.

Watch

Free resources

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