Design Patterns: the Gang of Four, thirty years on
Design Patterns: Elements of Reusable Object-Oriented Software was published in 1994. Its four authors — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — became known as the Gang of Four, and the book became known as GoF, and for most of the following decade a generation of object-oriented programmers read it, underlined it, and argued about it. Some of that argument was the book’s fault. Most of it was the reader’s: the patterns were treated, far too often, as a menu to apply rather than as a vocabulary to describe.
The authors were not proposing that every program contain a Visitor and a Factory and a Singleton. They were documenting solutions that experienced designers had already independently reinvented dozens of times, and giving those solutions names so that people could talk about them. “We need a Strategy here” is a much shorter sentence than “we need a family of interchangeable algorithms selectable at runtime behind a common interface,” and the two sentences mean the same thing. The book’s real contribution was that shared vocabulary.
Thirty years on, the twenty-three patterns have aged unevenly. Some are so fundamental that modern languages build them in — Iterator is syntax in every mainstream language now, and you no longer write a class to get one. Some, like Strategy and Decorator, are so useful that you write them unconsciously and only notice the pattern when someone asks what you just did. A few — Singleton is the famous case — have become cautionary tales. What follows is a tour of the ones that are still worth knowing, organized into the three categories the authors used: creational (how objects get made), structural (how objects are composed), and behavioral (how objects collaborate).
The book’s claim
Before the patterns themselves, it is worth being precise about what the GoF argued. Their thesis, borrowed from Christopher Alexander’s architectural work, was that good designs in a mature field recur. Faced with similar forces, competent designers converge on similar structures. If you can identify those structures, name them, and describe the forces that select for them, you give the next generation a head start: they can recognize the situation and reach for the known answer instead of rediscovering it.
Each pattern in the book is presented the same way: a name, an intent, a motivating problem, a structure (in UML), a set of participants and collaborations, consequences (what the pattern buys and costs), and a sample implementation. The format matters. A pattern is not a snippet; it is a named relationship between forces. Knowing when not to apply one is as important as knowing the mechanics.
Creational patterns
Creational patterns are about the fact that new ConcreteClass(...)
is not always what you want. Sometimes you want to hide which concrete
class is being created. Sometimes you want to share instances.
Sometimes the construction itself is complicated enough to deserve a
name. The creational patterns address those needs.
Factory Method
A class wants to create an object, but it wants its subclasses to decide which object. It declares a method that returns the abstract product type — the factory method — and subclasses override that method to return whichever concrete product fits.
This is the pattern that lets you write a Dialog class whose
createButton() returns an abstract Button, and subclasses
WindowsDialog and MacDialog override createButton() to return
WindowsButton and MacButton respectively. The dialog’s layout code
never knows which button it got, and adding a third platform does not
require editing the dialog.
Factory Method earns its keep anywhere construction needs to vary without the calling code varying. Its modern shape is often smaller than the book’s — a function parameter, a registered builder, a dependency-injected provider — but the relationship is the same: separate who decides what to build from who uses what was built.
Abstract Factory
Abstract Factory is Factory Method’s older sibling, and it answers a
related but distinct question: what if I need to create a whole
family of related products, and all of them must come from the same
variant? A UI toolkit that renders on Windows, macOS, and Linux does
not just need a button factory; it needs a button, a scrollbar, a
menu, a window, and a text field, all of the same look. Mixing a
WindowsButton with a MacScrollbar is a bug.
Abstract Factory captures that constraint. A single WidgetFactory
interface declares createButton(), createScrollbar(),
createMenu(), and so on, and each platform implements the whole
family. The calling code picks a factory once and all subsequent
objects come from the same family by construction.
The pattern is heavier than Factory Method and less often justified. It shines when the family constraint is real — cross-platform UIs, multi-database persistence layers, multi-vendor cloud abstractions — and feels excessive when products could be chosen independently.
Builder
Builder separates the construction of a complex object from its representation, so the same construction process can produce different representations. In practice, it is the pattern you reach for when a constructor has started to grow arguments faster than your memory can track them, and when the order or presence of those arguments is itself part of the contract.
The modern shape of Builder is usually fluent: Query.select("id", "name").from("users").where("active").orderBy("id").build(). Each
call returns a builder with slightly more state, and a final
build() returns the immutable product. The point is not the fluent
syntax — that is cosmetic — but the separation between a mutable
scaffold used during construction and the immutable object that
results.
Builder is most useful when construction has optional parts, conditional parts, or ordering rules that the constructor cannot enforce without combinatorial overloads. For a three-argument constructor, it is overkill. For a query, a configuration, a message, or anything with a dozen optional fields, it is the sanest answer.
Singleton
Singleton guarantees that a class has exactly one instance and provides a global point of access to it. It is the most famous GoF pattern and the most widely regretted. The mechanics are trivial — a private constructor, a static accessor, a guarded single instance — and the problems are not.
Singletons are global mutable state with a more respectable name. They make testing hard, because a test cannot substitute the instance without leaking into the next test. They make parallelism hard, because any shared mutable state must be synchronized. They encourage code to reach across the architecture to grab the singleton directly, which couples everything to its existence. Most uses of Singleton in practice are cases where the thing being made singular is actually a dependency that should be injected, and the singleton reference is a shortcut that papers over missing plumbing.
The pattern still has legitimate uses — truly process-wide resources like a logger configuration or a connection pool — but the contemporary advice is: prefer dependency injection of a single instance managed at the composition root over a Singleton class. The cardinality is the same; the testability is not.
Structural patterns
Structural patterns are about how classes and objects compose to form larger structures. They answer the question: I have two pieces that do not quite fit, or a tree of pieces to navigate, or a layer to wrap around something — what shape should the composition take?
Adapter
Adapter converts the interface of a class into another interface a client expects. It is the pattern of the translation layer, and it is one of the most universally applicable patterns in the book.
You have an existing component — a legacy class, a third-party SDK, a module written for a different conceptual model — whose interface does not match what your calling code wants to speak. Rather than changing either side, you write an adapter that sits between them, taking calls in the consumer’s vocabulary and forwarding them, with any necessary translation, to the adaptee.
Adapter is what the anti-corruption layer in DDD is made of, under
the hood. It is what lets you wrap a java.io.InputStream around
something that isn’t really a stream, or expose a REST client
through the same interface your fake test double implements. It is
cheap, local, and keeps the mess contained at the boundary instead of
letting it leak inward.
Decorator
Decorator attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending functionality. It is the pattern where behavior is added by wrapping rather than inheriting.
A BufferedInputStream wraps an InputStream and adds buffering; a
GZIPInputStream wraps a stream and adds decompression; a
CipherInputStream wraps a stream and adds decryption. The wrapping
is compositional: you can compose them in any order to get buffered
decryption of a gzipped network read, and none of the classes needed
to anticipate the others.
The power of Decorator is that it sidesteps the combinatorial explosion of subclassing. If you had to provide every combination of buffering, compression, and encryption as a distinct subclass, you would have eight classes for three features, sixteen for four, and so on. With decorators, you have one class per feature and combine them at runtime.
Decorator is most alive in stream processing, middleware pipelines (HTTP handlers, request filters, logging wrappers), and any situation where a behavior is genuinely additive and composable.
Composite
Composite lets clients treat individual objects and compositions of
objects uniformly. It is the pattern behind every tree you have ever
walked: a filesystem where files and directories both respond to
size(), a UI where leaves and containers both respond to render(),
an expression tree where literals and operators both respond to
evaluate().
The key move is a shared abstraction — Component, in the book’s
vocabulary — that both leaf nodes and internal nodes implement. The
internal nodes hold children of the same abstract type and usually
implement operations by recursing into them. Client code traverses
the structure without ever needing to know whether a given node is a
leaf or a branch; that is the pattern’s whole point.
Composite earns its keep whenever the data is genuinely recursive and clients need to act on the structure polymorphically. It is overengineered when the structure is flat and you just happen to have a list.
Facade
Facade provides a unified, simplified interface to a set of interfaces in a subsystem. It is the small front door on a large building.
You have a subsystem — a library, a cluster of collaborating classes, a remote service with a sprawling API — and most callers do not need its full flexibility. They need three or four common operations. A facade packages those operations behind a single narrow interface, so most of the codebase can depend on the narrow surface and ignore the rest.
Facade is one of the cheapest patterns to apply and among the most valuable for codebase health. It buys you the ability to evolve the subsystem behind the facade without churning every caller, and it communicates to future readers which operations are the supported front door and which are implementation details. Most well-designed packages have a facade at their top level, even if nobody calls it that.
Proxy
Proxy provides a surrogate or placeholder for another object to control access to it. It looks like Adapter — both are objects that stand in front of another object and forward calls — but the intent is different: Proxy preserves the interface, it doesn’t translate it.
Proxies appear in several flavors the book named and modern systems still rely on. A remote proxy represents an object that lives in another process or another machine, hiding the network from callers — the foundation of RPC. A virtual proxy defers creation of an expensive object until it is actually used — the foundation of lazy loading in ORMs and the image placeholders in document editors. A protection proxy wraps an object with access checks — the foundation of most authorization interception.
The pattern is everywhere in infrastructure, usually below the surface. The lesson is that when you want to intercept all access to an object without changing the object or its callers, interposing a proxy is the named, well-understood move.
Behavioral patterns
Behavioral patterns are about how objects communicate, how responsibilities are distributed, and how control flows through a system. They are the richest category in the book, and the one where the vocabulary has done the most to shape how programmers talk about design.
Strategy
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The client code holds a reference to the abstract strategy interface and uses it; the concrete algorithm is chosen separately and swapped freely.
A sort routine that takes a Comparator is using Strategy. A
compression utility that takes an algorithm selector is using
Strategy. A pricing engine that looks up a promotion rule and applies
it is using Strategy. The pattern is so common in contemporary code
that it rarely announces itself; first-class functions have
subsumed most of its mechanics, and a strategy is often just a
function passed as an argument. The design move it encodes —
extract varying behavior into a replaceable piece — is still one of
the most useful refactors in object-oriented design.
Strategy is the honest alternative to sprawling if/else chains on
type codes. The next time you see a twelve-branch switch that
dispatches on an enum and does subtly different things in each arm,
the right answer is almost always a Strategy.
Observer
Observer defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. It is the pattern underneath events, signals, reactive streams, and every UI toolkit’s change-notification system.
The subject maintains a list of observers and exposes methods to attach and detach them. When its state changes in a way observers care about, it iterates the list and calls a notification method on each. Observers react however they choose, independently of each other and of the subject.
Observer’s real contribution was making explicit the cost of coupling
senders to receivers. A subject that directly called its
dependents would have to know them all; an observer-based subject
does not. That decoupling is what lets modern reactive systems scale
to complex event graphs without the source of each event having
knowledge of every consumer. Publish-subscribe systems, event buses,
and every on(...) handler in every framework are Observer’s
descendants.
The pattern’s main hazard — which the GoF named — is unexpected update cascades. An observer that updates state that is observed by another observer that updates state observed by the first observer is a loop. Event-driven systems have to be careful about feedback, and the usual response is to make the update graph explicit: topological ordering, scheduled flushes, or explicit queueing.
Command
Command encapsulates a request as an object, letting you parameterize clients with different requests, queue or log requests, and support undoable operations. It is the pattern where the invocation itself becomes data.
Once a request is an object, you can put it in a list (a queue, a batch, a transaction log). You can store it on a stack (an undo history). You can serialize it (a cross-process command). You can attach metadata to it (who issued it, when, with what permissions). All of these are impossible if the request is just a method call, because a method call is an event that happens and leaves no trace.
Command is the pattern behind undo/redo systems, command buses in CQRS architectures (where it meets DDD directly), task queues, macro systems, and scripting layers. The investment of turning a method call into an object pays for itself the moment you need to do anything with the invocation other than invoke it.
State
State allows an object to alter its behavior when its internal state
changes. The object appears to change its class. It is what you
reach for when you find yourself writing large conditional blocks
that switch on an enum-valued status field and do substantially
different things in each branch.
A Document that can be draft, in review, approved, or
published has four modes that respond differently to the same
operations — edit, submit, approve, publish. Without State,
each operation is a switch on the current status. With State, each
status is its own class implementing a common interface, and the
document delegates each operation to the current state object, which
may also replace itself with a new state as a side effect.
State is the pattern’s object-oriented way of saying finite state
machine. The win is not expressiveness — any state machine can be
written with enums and switches — but locality: everything about
what happens in the reviewing state lives in the Reviewing class,
and adding a fifth status is a new class, not a modification of every
operation’s switch statement.
Template Method
Template Method defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. The skeleton decides the order and the control flow; the subclasses fill in the specifics.
It is the simplest of the behavioral patterns and the most directly
tied to classical inheritance. Testing frameworks have always used it:
setUp, run the test method, tearDown — the framework controls the
order; the subclass fills in the steps. Data pipelines use it: open
the input, read records, transform, write, close — framework-controlled
shape, subclass-controlled specifics.
Template Method has aged into a slightly less common pattern because composition — a pipeline object parameterized with callbacks — can often replace it with less coupling. The value of the pattern is still there, though: when the algorithm’s structure is stable and only specific steps vary, a template method communicates that intent clearly.
Iterator
Iterator provides a way to access the elements of an aggregate object
sequentially without exposing its underlying representation. In 1994,
writing an iterator meant writing a class with hasNext() and
next() methods and being careful about concurrent modification. In
2026, for (x of collection) in JavaScript, for x in collection in
Python, for x := range collection in Go, and equivalents in every
mainstream language compile to exactly this pattern, usually through
a language-level protocol rather than an explicit class.
Iterator is the book’s clearest case of a pattern becoming so successful it stopped being a pattern. Its lesson — that how a collection is traversed should be decoupled from what the collection is — is so thoroughly baked into modern language design that programmers who have never read the book still rely on it several times a day.
Chain of Responsibility
Chain of Responsibility passes a request along a chain of handlers until one of them handles it. Each handler decides whether to act on the request or forward it to the next handler in the chain, without the sender needing to know which handler will ultimately respond.
Middleware pipelines in HTTP frameworks are the pattern’s modern face. A request passes through authentication, then rate limiting, then logging, then routing, then the actual handler; each stage may short-circuit the request or decorate it and pass it along. Event handler hierarchies in UI frameworks — where a click bubbles from the clicked element up to the root until something handles it — are the same pattern.
The pattern is useful when who should handle the request is not known by the sender and may depend on runtime state. It is overengineered when the handler is obvious; a plain method call is cheaper and clearer.
What the book got right
The book’s biggest correct bet was that a shared vocabulary for design moves would be more valuable than any specific pattern in isolation. That bet paid off. When a reviewer comments that a piece of code “should be a Strategy, not a switch,” or when a designer sketches a Facade to protect a legacy subsystem, they are using the GoF vocabulary. Most of the programmers in that conversation have probably never read the book cover to cover, but the language has diffused into the profession anyway.
The second thing the book got right was its emphasis on consequences. Every pattern in GoF is followed by a discussion of what it costs, what it rules out, and when it is wrong to apply. The pattern catalog is not a menu of good ideas; it is a catalog of tradeoffs. Readers who absorbed that part of the book came away better designers. Readers who skipped it came away with twenty-three hammers and an urge to find nails.
What hasn’t aged well
Some of the patterns’ details are showing their age. The book is written in C++ and Smalltalk, and some of its structural advice is compensating for the absence of language features that later became standard. Iterator, as noted, is now syntax. Strategy is often just a lambda. Observer is a built-in in most reactive frameworks. First-class functions, closures, generic types, and pattern-matching have collectively absorbed a surprising fraction of the book’s mechanics into the language level, leaving the intent of each pattern intact but making the implementation much lighter.
The book is also a product of its moment’s enthusiasm for deep inheritance hierarchies. Several patterns — Template Method, the inheritance flavor of Factory Method, the classical presentation of Abstract Factory — are pitched around subclassing as the natural extension mechanism. The subsequent twenty years of language design and architectural experience have pushed hard in the other direction: composition over inheritance is, by some margin, the industry’s preferred default now, and many pattern implementations have migrated from subclassing to parameterization with objects, functions, or configuration.
Singleton deserves its own line in this column. The pattern as described is technically correct and widely regretted. Modern practice treats instance cardinality as a concern of the composition root — the place where objects are wired together at startup — and keeps the classes themselves ignorant of whether there is one of them or a thousand.
What’s still essential
The patterns worth internalizing, thirty years after publication, roughly in order of how often the vocabulary earns its keep:
Strategy, Observer, Decorator, Adapter, Facade, Composite, Command, State, Factory Method, Proxy, Template Method, Chain of Responsibility. These are the twelve that most designers use by name in most object-oriented (and increasingly non-object-oriented) systems. The other eleven are worth knowing about; they are not worth drilling on.
The deeper lesson, though, is not the list. The deeper lesson is the posture: when you find yourself about to write a sprawling conditional on a type code, a multi-step construction with eight optional parameters, or a wrapper that subtly changes behavior — someone has named this move. The named solution is not always right, but recognizing that you are in a familiar situation lets you reach past the first instinct and ask whether the known tradeoffs apply.
Design Patterns is a reference. It is not a syllabus, and it is not an endorsement. Read it for the vocabulary, for the tradeoff discussions, and for the humility it models. The patterns were there before the book was written, and the book did not invent them; it watched people who were already good at design and wrote down what they did. The modern job is the same. Look at code that feels right, notice what moves are being made, and give those moves names — the ones the Gang of Four recorded, the ones the community has added since, and the ones that nobody has named yet but that you’ll recognize in six months when you make them again.