← Back to blog
Architecture

GoF Design Patterns: complete guide with diagrams

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

Have a project in mind?

Let's talk about your challenges and see how Gotan can help.

Contact us