The 23 patterns of the Gang of Four remain the common grammar of object-oriented design. This illustrated guide covers the most useful patterns in practice, with their structures, use cases and pitfalls to avoid.
The book Design Patterns: Elements of Reusable Object-Oriented Software published in 1994 by Gamma, Helm, Johnson and Vlissides β the "Gang of Four" β remains one of the most cited references in software engineering. Its 23 patterns are divided into three families: creational (how to create objects), structural (how to assemble them) and behavioral (how they communicate). This guide focuses on the most useful patterns in practice, avoiding the exhaustive catalogue that often leads to mechanical pattern-matching.
Creational Patterns
Factory Method
The Factory Method delegates object creation to subclasses, allowing a class to work with types whose concrete implementation it doesn't know.
Creator
ββββββββββββββββββββ
+ factoryMethod() : Product β abstract
+ operation()
β²
βββββββββββ΄βββββββββββ
β β
ConcreteCreatorA ConcreteCreatorB
+ factoryMethod() + factoryMethod()
β β
βΌ βΌ
ProductA ProductB
(implements Product) (implements Product)
Use case: a parsing framework supporting JSON, XML and CSV. The abstract Parser class defines createReader() β
each subclass returns the appropriate reader. The processing algorithm remains identical; only the reader creation
varies. Avoid when there's only one concrete type: it's then unnecessary indirection.
Builder
The Builder separates the construction of a complex object from its representation, allowing different representations to be created with the same construction process.
Director
ββββββββββββββββββββββ
- builder : Builder
+ construct() βββββββΊ Builder (interface)
ββββββββββββββββββββββ
+ buildPartA()
+ buildPartB()
+ getResult() : Product
β²
ββββββββββββ΄ββββββββββββ
ConcreteBuilderA ConcreteBuilderB
+ buildPartA() + buildPartA()
+ buildPartB() + buildPartB()
+ getResult() + getResult()
Practical use case: constructing a complex SQL query, an email with attachments and multiple recipients, or an
object configuration with dozens of optional parameters. The fluent Builder (each method returning this) is its most
common modern form in Java:
Email email = new EmailBuilder()
.to("alice@example.com")
.subject("Monthly report")
.body(bodyHtml)
.attachment(reportPdf)
.build();
Singleton
The Singleton ensures a class has only one instance and provides a global access point to it.
Singleton
ββββββββββββββββββββββββββ
- instance : Singleton (static)
- Singleton() (private)
ββββββββββββββββββββββββββ
+ getInstance() : Singleton (static)
+ operation()
The Singleton is the most controversial GoF pattern. It makes testing harder (implicit global coupling), complicates parallelization, and hides dependencies. In modern Java, prefer dependency injection via Spring β the IoC container manages the lifecycle. Reserve the Singleton for the rare cases where a single instance is a real system constraint (access to a unique hardware resource, for example).
Structural Patterns
Adapter
The Adapter converts the interface of a class into another interface the client expects. It allows incompatible classes to collaborate.
Client βββββββΊ Target (interface)
+ request()
β²
Adapter
ββββββββββββββββββ
- adaptee : Adaptee
+ request() βββββββΊ adaptee.specificRequest()
Adaptee
ββββββββββββββββββ
+ specificRequest()
Use case: integrating a third-party library whose interface doesn't match your domain, adapting a legacy service to a new interface, or plugging an external logging library into your internal interface. The composition Adapter (above) is preferable to inheritance Adapter β more flexible and less fragile.
Decorator
The Decorator dynamically attaches additional responsibilities to an object, as an alternative to inheritance for extending functionality.
Component (interface)
+ operation()
β²
ββββββββββββ΄βββββββββββββββ
β β
ConcreteComponent Decorator
+ operation() - component : Component
+ operation() β component.operation()
β²
ββββββββββββ΄βββββββββββ
DecoratorA DecoratorB
+ operation() + operation()
// + extra behavior // + extra behavior
Use case: Java's InputStream classes are the canonical example β BufferedInputStream decorates
FileInputStream which decorates InputStream. In practice: adding logging, caching, or validation to a service
without modifying existing implementations. The Decorator allows stacking behaviors at runtime, unlike inheritance
which fixes the combination at compile time.
Facade
The Facade provides a simplified interface to a complex subsystem, without modifying the subsystem itself.
Client
β
βΌ
Facade
ββββββββββββββββββββββββββββββββββββββββββ
+ operation()
β β β
βΌ βΌ βΌ
SubsystemA SubsystemB SubsystemC
(complex) (complex) (complex)
Use case: an OrderService that orchestrates InventoryService, PaymentService, NotificationService and
ShippingService. The client only calls orderService.placeOrder(cart) β the complexity is encapsulated. The Facade
doesn't lock access to subsystems: an advanced client can still call PaymentService directly if needed.
Behavioral Patterns
Observer
The Observer defines a one-to-many relationship between objects: when one object changes state, all its dependents are notified automatically.
Subject (interface) Observer (interface)
ββββββββββββββββββ ββββββββββββββββββ
+ attach(Observer) + update()
+ detach(Observer) β²
+ notify() β
β² ConcreteObserverA
β ConcreteObserverB
ConcreteSubject + update()
- state
- observers : List<Observer>
+ setState()
+ notify() β forEach(o β o.update())
Use case: event systems, UI listeners, reactive architectures. In Java, it's the basis of
java.util.EventListener. In Angular, EventEmitter and RxJS Subject are modern implementations. Watch out for the
lapsed listener problem: if observers don't properly unsubscribe, they remain in memory even when the component is
destroyed β a classic source of memory leaks.
Strategy
The Strategy defines a family of algorithms, encapsulates each one and makes them interchangeable. It allows the algorithm to vary independently from clients that use it.
Context
ββββββββββββββββββββββ
- strategy : Strategy
+ setStrategy(Strategy)
+ executeStrategy() βββββββΊ Strategy (interface)
+ execute(data)
β²
ββββββββββββββΌβββββββββββββ
StrategyA StrategyB StrategyC
+ execute() + execute() + execute()
Use case: different sorting algorithms depending on data size, different pricing strategies (normal price, sales,
premium subscription), different export modes (PDF, CSV, Excel). Strategy replaces a cascade of if/else or switch
in the context, delegating the choice to configuration or runtime. In modern Java, lambdas and functional interfaces
make the pattern very lightweight to implement.
Command
The Command encapsulates a request as an object, allowing actions to be parameterized, queued, logged and undone.
Invoker Command (interface)
ββββββββββββββββββ ββββββββββββββββββ
- command : Command + execute()
+ setCommand(Command) + undo()
+ run() β command.execute() β²
βββββββββ΄ββββββββ
CommandA CommandB
- receiver - receiver
+ execute() + execute()
+ undo() + undo()
β
Receiver
+ action()
Use case: undo/redo systems in editors, task queues, audit logging. Command is at the heart of CQRS architecture
where every system mutation is an explicit command. The optional undo() method is powerful but requires capturing
state before execution.
Template Method
The Template Method defines the skeleton of an algorithm in a method, letting subclasses redefine certain steps without changing its structure.
AbstractClass
ββββββββββββββββββββββββββββββββββ
+ templateMethod() β final
β
ββ step1() β implemented (invariant)
ββ step2() β abstract (variant)
ββ step3() β implemented (invariant)
ββ hook() β optional (can be overridden)
β²
ββββββββββ΄βββββββββ
ConcreteA ConcreteB
+ step2() + step2()
+ hook()
Use case: JUnit test lifecycle (setUp, test method, tearDown), file processing (open, specific parsing, close),
ETL pipelines. Template Method is particularly effective for imposing an execution protocol while allowing
customization of variable steps.
Choosing the right pattern
The classic trap is looking for a recognizable pattern to apply rather than solving the actual problem. Some heuristics:
- Runtime variability β Strategy, State, Command
- Complex object construction β Builder, Factory Method, Abstract Factory
- Extension without modification β Decorator, Visitor
- Interface simplification β Facade, Adapter
- Object communication β Observer, Mediator
Code that uses no patterns isn't necessarily bad. Code that uses them everywhere almost certainly is. Patterns are a shared vocabulary for naming known solutions β their real value is communication, not mechanical implementation.
β See also: SOLID principles Β· Clean Architecture Β· Hexagonal architecture