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.