Software Engineering

Design Patterns

Reusable solutions to recurring software design problems, catalogued by the Gang of Four into creational, structural, and behavioral categories.

⏱ 12 min read

What it is

Design patterns are general, reusable solutions to commonly occurring software design problems. They were popularised by the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — collectively known as the Gang of Four (GoF). The book catalogues 23 patterns grouped into three categories: Creational (how objects are created), Structural (how objects are composed), and Behavioral (how objects communicate and distribute responsibility).

Patterns are not copy-paste code snippets; they are higher-level descriptions of intent. A pattern captures the essential structure of a solution — the participants, their roles, and the collaboration between them — while leaving implementation details to the programmer. They also provide a precise vocabulary: saying "use a Strategy here" communicates far more than describing the resulting code structure in prose.

Why it exists

As OOP became mainstream in the late 1980s and early 1990s, teams repeatedly solved the same structural problems in idiosyncratic ways. Code reviews became difficult because reviewers had no shared vocabulary to describe intent. The GoF book addressed this by formalising recurring solutions that experienced developers had independently discovered, giving them canonical names and documenting their trade-offs.

Patterns also accelerate design by providing proven starting points. Rather than reasoning from first principles why a plugin system should use a Factory Method, a developer familiar with patterns can immediately apply the canonical structure and focus on domain specifics. They also serve as architectural signposts — finding a Singleton in a large codebase tells you that object is expected to be globally unique and shared, which is a significant constraint worth communicating explicitly.

When to use

  • When you recognise a recurring structural problem that matches a known pattern's intent — use the pattern's name in code (class names like ConnectionPoolFactory, LoggingDecorator).
  • When designing plugin or extension systems (Factory Method, Abstract Factory, Strategy).
  • When adding functionality to classes you cannot or should not modify (Decorator, Adapter).
  • When multiple objects need to react to state changes (Observer/Event Listener).
  • When encapsulating a family of algorithms that must be interchangeable at runtime (Strategy).
  • When the construction of a complex object should be independent of the parts that make it up (Builder).

When not to use

  • Pattern-first design: Choosing a pattern before understanding the problem causes unnecessary complexity — "when all you have is a hammer" antipattern.
  • Simple one-off code: A 20-line script does not need an Abstract Factory. Patterns are justified by complexity and change frequency.
  • Singleton for convenience: The Singleton pattern is frequently misused as a global variable mechanism; prefer dependency injection instead.
  • Languages with built-in support: Some patterns exist to work around language limitations; in Python/JavaScript, many structural patterns are unnecessary because of first-class functions, duck typing, and mixins.

Typical architecture


CREATIONAL PATTERNS
───────────────────
Factory Method:  Creator ──► «interface» Product ◄── ConcreteProduct
                    └── ConcreteCreator.createProduct() → ConcreteProduct

Builder:         Director → Builder.buildPart() → Builder.getResult() → Product
                 Allows step-by-step object construction; useful for complex configs.

Singleton:       Singleton.getInstance() returns cached _instance
                 ⚠ Use only when a single global coordinator is truly required.

STRUCTURAL PATTERNS
───────────────────
Adapter:         Client → Target(method()) ← Adapter(method() { adaptee.oldMethod() })
                 Adapts incompatible interfaces without touching the adaptee.

Decorator:       Component ◄── ConcreteComponent
                            ◄── Decorator(wraps Component, adds behaviour)

Facade:          Client → Facade → [SubsystemA, SubsystemB, SubsystemC]
                 Simplifies a complex subsystem with a high-level interface.

Proxy:           Client → Proxy(implements Subject) → RealSubject
                 Adds access control, caching, or lazy loading transparently.

BEHAVIORAL PATTERNS
───────────────────
Observer:        Subject.register(Observer) → Subject.notify() → Observer.update()
                 Push-based event propagation; backbone of event-driven UIs.

Strategy:        Context(strategy: Strategy) → strategy.execute()
                 Swap algorithms at runtime; eliminates large switch statements.

Command:         Invoker → Command.execute() → Receiver.action()
                 Encapsulates requests as objects; enables undo/redo queues.

Template Method: AbstractClass.templateMethod() calls primitive operations
                 Concrete subclasses override steps without changing the algorithm.

Pros and cons

Pros

  • Provides a proven vocabulary that speeds up design discussions and code reviews.
  • Encodes decades of collective experience — avoids reinventing solutions to known problems.
  • Promotes loose coupling and high cohesion when applied appropriately.
  • Makes intent explicit — a class named CachingProxy communicates its role immediately.
  • Eases onboarding — developers familiar with GoF can read an unfamiliar codebase faster.

Cons

  • Risk of over-engineering — forcing patterns onto simple code adds indirection without benefit.
  • Pattern names vary across languages and communities; "Observer" in Java is different from event emitters in Node.js despite the same intent.
  • Some patterns are workarounds for language limitations and are anti-patterns in more expressive languages.
  • Increases class count, which can overwhelm new developers unfamiliar with the pattern.
  • Misidentification — applying the wrong pattern causes subtle design problems that are hard to diagnose.

Implementation notes

Creational patterns: Factory Method delegates instantiation to subclasses, enabling subclasses to decide which class to instantiate. Use it when a superclass cannot anticipate the class of objects it needs to create. Abstract Factory creates families of related objects without specifying concrete classes — useful for UI themes or database drivers. Builder separates a complex object's construction from its representation; it shines for objects with many optional parameters (avoiding telescoping constructors). Prototype clones existing objects instead of creating new ones, useful when construction is expensive. Singleton should be a last resort — prefer DI containers to manage lifecycle.

Structural and behavioral patterns: Decorator is preferable to inheritance for adding responsibilities because it composes at runtime and does not cause a class explosion. Adapter and Facade are both wrappers, but Adapter changes an interface to match what a client expects, while Facade provides a simpler interface to a complex system. Observer is the backbone of reactive systems but can cause memory leaks if observers are not deregistered; modern implementations use weak references or explicit disposal tokens. Strategy eliminates large if/else or switch chains by making algorithms first-class objects — TypeScript's function types make this particularly clean without needing full interface hierarchies.

Common failure modes

  • Singleton abuse: Using Singleton as a global variable store defeats testability and creates hidden coupling between modules.
  • Premature abstraction via Factory: Creating factory hierarchies before there is more than one implementation adds indirection with no pay-off.
  • Observer memory leaks: Forgetting to unsubscribe observers in long-lived objects (especially UI components) causes memory leaks and ghost updates.
  • Deep decorator stacks: Chaining 10 decorators creates debugging nightmares; consider a pipeline or chain-of-responsibility pattern instead.
  • Strategy without context: Injecting a strategy but never varying it is a YAGNI violation; use a concrete implementation until you actually need to swap.

Decision checklist

  • Have you identified a specific recurring design problem (not just "I want to use a pattern")?
  • Does the pattern's stated intent match your problem, not just the structure?
  • Are you adding behaviour that varies independently from the class it belongs to? (→ Strategy/Decorator)
  • Do you need to create families of related objects that must be used together? (→ Abstract Factory)
  • Does client code need to be notified of state changes without polling? (→ Observer)
  • Are you working around an incompatible interface you cannot change? (→ Adapter)

Example use cases

  • Report generation: Builder constructs reports with optional sections; Strategy selects PDF, HTML, or CSV rendering; Decorator adds watermarking or encryption.
  • Notification pipeline: Observer notifies subscribers (email, SMS, push) when an order ships; Command encapsulates each notification as a retriable job.
  • Database access layer: Factory Method selects the right repository implementation; Proxy adds query caching; Adapter bridges a legacy DAO interface to a new repository contract.
  • SOLID Principles — Design patterns are often direct implementations of SOLID principles, especially OCP and DIP.
  • Domain-Driven Design — DDD's Repository, Factory, and Specification patterns are domain-specific refinements of GoF patterns.
  • Refactoring Patterns — Refactoring moves code toward pattern structures incrementally; "Replace Conditional with Polymorphism" is a path to Strategy.

Further reading