LESSON 4.1
Introduction to design patterns
A shared architectural vocabulary for object-oriented systems: how to evaluate, select, and avoid over-engineering design patterns.
10 min read
A design pattern is not a library you install from a package manager, nor is it an algorithmic recipe like quicksort or Dijkstra. A design pattern is a named, battle-tested solution to a recurring design problem in object-oriented software. It establishes a high-bandwidth vocabulary: saying "we used an Observer between the order checkout and email delivery" instantly communicates intent, decoupling, and structure without needing to explain sixty lines of glue code.
Patterns describe the relationships between classes, interfaces, and collaborating objects. They capture hard-won architectural trade-offs, making explicit what is flexible and what is rigid. Patterns do not provide copy-paste code; rather, they provide conceptual blueprints that must be tailored to the specific domain rules, language idioms, and concurrency constraints of your application.
The Gang of Four and the three families
In 1994, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (collectively known as the Gang of Four, or GoF) published Design Patterns: Elements of Reusable Object-Oriented Software. Drawing inspiration from Christopher Alexander's architectural pattern language for physical buildings, the authors codified 23 classic software design patterns. They categorized these 23 patterns into three fundamental families based on their underlying architectural intent:
- Creational patterns: Deal with object creation mechanisms, isolating client code from concrete instantiation logic, complex constructor graphs, and family dependencies (Singleton, Factory Method, Abstract Factory, Builder, Prototype).
- Structural patterns: Deal with the composition of classes and objects into larger, flexible structures while maintaining loose coupling and clean boundaries (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy).
- Behavioral patterns: Deal with algorithms, communication contracts, and the clean assignment of responsibilities between collaborating objects (Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor).
Anatomy of a pattern
When evaluating any pattern in architecture reviews or technical interviews, dissect it across four core elements:
- Intent: A single sentence stating the goal, purpose, and design principle addressed by the pattern.
- Problem and motivation: The exact architectural friction or code smell the pattern resolves, including why naive implementations fail as requirements evolve.
- Participants and relationships: The roles played by classes and interfaces (client, abstraction, concrete implementors) and how they collaborate.
- Consequences and trade-offs: What flexibility is gained, and what operational costs (additional indirection, memory allocations, learning curve) are incurred.
Symptom to pattern selector
Patterns should be selected in response to concrete architectural pain points, never pre-emptively. Use this symptom diagnostic table to identify candidate patterns:
| Symptom in codebase | Candidate pattern | Primary mechanism |
|---|---|---|
Multiple if/else or switch branches checking an entity type or pricing tier | Strategy | Extract algorithmic variants into polymorphic classes behind a common interface |
| Object transitions between states with distinct behavior in each phase | State | Represent each lifecycle state as an independent class receiving actions |
| Object construction scattered across callers with complex dependencies | Factory Method or Abstract Factory | Centralize creation logic behind a dedicated creator interface |
| Constructor has 6+ parameters, many optional or boolean flags | Builder | Step-by-step construction with named fluent methods producing an immutable instance |
| Need to add responsibilities to an object dynamically without subclass explosion | Decorator | Wrap the component with matching interface and delegated forward calls |
| Incompatible existing interface must cooperate with a modern consumer | Adapter | Translate method calls and data contracts between caller and legacy target |
| Complex subsystem of 10+ interacting classes hard for clients to use | Facade | Provide a simplified high-level entrypoint orchestrating the subsystem |
| UI components or distributed services need notification of state changes | Observer | Subject maintains subscriber callbacks without knowing concrete subscriber types |
| Operations require undo, redo, queuing, or delayed execution | Command | Encapsulate request parameters and receiver bindings inside command objects |
| Tree hierarchy of leaf nodes and containers must be treated uniformly | Composite | Common interface implemented by both primitives and composite collections |
| Millions of fine-grained objects causing excessive heap memory consumption | Flyweight | Share immutable intrinsic state across instances while passing extrinsic state |
| Expensive resource initialization needs on-demand lazy loading or access check | Proxy | Intercept target calls with a surrogate controlling access, cache, or virtualization |
Patterns in practice: decoupling callers
Consider a simple notification dispatcher in a production system. Without patterns, callers directly instantiate and switch over concrete delivery mechanisms, forcing changes whenever a new delivery channel is added. By introducing polymorphic dispatchers behind a common contract, the service coordinates messaging without coupling to delivery details.
Email: System alert | SMS: System alertThe danger of pattern overuse
The most pervasive anti-pattern in junior-to-mid engineering is patternitis: forcing design patterns into code that does not need them.
Introducing three layers of factories, adapters, and strategies for a simple CRUD operation that will never vary adds pure friction. Every layer of abstraction costs cognitive load, navigation complexity in IDEs, and indirect call overhead. Code that can be written in five readable lines should not be expanded into seven classes across three packages simply because a textbook describes an abstract factory.
As explored in depth in module 41 on anti-patterns and refactoring, the golden rule of clean design is: start with the simplest working object model that enforces domain invariants, and refactor toward patterns only when extension points prove necessary.
Patterns in machine-coding interviews
In 60-to-90 minute machine coding rounds (detailed in module 42), interviewers assess your ability to design resilient, extensible domain models under time constraints.
Interviewers do not reward candidate code that blindly strings together every pattern from memory. Instead, top candidates:
- Identify invariants first: Define domain entities with encapsulated state and validation before writing behavioral glue.
- Design to interfaces: Place contracts at boundaries expected to change (e.g., fee calculation strategies, notification observers).
- Keep creation separate from use: Use factories or builders to construct valid domain graphs.
- Maintain deterministic execution: Pass dependencies (like clocks and storage repositories) rather than relying on global singletons.
Throughout the following modules, each pattern is examined from first principles: the naive problem, step-by-step refactoring, runnable JavaScript and Java implementations, real production ecosystem usages, and interview evaluation rubrics.
Try it
Configuration Loader LLD
Merge configuration sources with provenance, validation and atomic reload.