Classic Software Design Patterns
What it is
Software design patterns are named, reusable solutions to recurring design problems. A pattern is not a library or a copy-paste template; it is a trade-off with a known intent, forces, structure, and consequences.
How it works
Patterns organize design choices by the job they do. Use a creational pattern when code must create or copy objects safely, a structural pattern when objects must compose without exposing their internals, and a behavioral pattern when objects need to distribute or vary behavior.
The following classes show a small notification flow. The Subject can notify observers, each Observer handles a different reaction, and a Strategy supplies the channel-specific behavior. The classes are not mandatory; they are a vocabulary for discussing the roles.
classDiagram
class Subject
class Observer
class EmailObserver
class SmsObserver
class Strategy
class NotificationStrategy
class ChannelStrategy
Subject --> Observer : notifies
Observer <|.. EmailObserver
Observer <|.. SmsObserver
Subject --> Strategy : uses
Strategy <|.. NotificationStrategy
NotificationStrategy <|.. ChannelStrategy
Creational patterns
- Factory Method — a creation method lets a subclass choose a product without the caller naming the concrete constructor.
- Abstract Factory — one factory creates a compatible family of products, such as dark-theme and light-theme UI controls.
- Builder — assembles a valid object through readable steps when a constructor or initializer would have too many or too fragile parameters.
- Singleton — controls access to one shared instance; use it only when global identity is truly a requirement, because hidden state makes tests and coordination harder.
- Prototype — creates a new object by copying an existing template, useful when object creation is expensive or the object graph is variable.
Structural patterns
- Adapter — translates one interface into another so an existing client can use an incompatible provider.
- Bridge — separates an abstraction from its implementation so both sides can vary independently.
- Composite — treats individual objects and groups of objects through one common interface, as in a file tree.
- Decorator — adds behavior by wrapping an object while keeping its interface.
- Facade — provides a simple entry point to a subsystem with many collaborators.
- Proxy — stands in for an object to control access, add caching, manage a remote boundary, or defer creation.
Behavioral patterns
- Chain of Responsibility — passes a request through handlers until one handles it.
- Command — represents a request as an object, which can be queued, logged, or undone.
- Iterator — provides a uniform way to traverse a collection without exposing its representation.
- Mediator — coordinates collaborators through a central object instead of direct peer-to-peer calls.
- Memento — captures state so it can be restored later without exposing its internals.
- Observer — notifies dependents when a subject changes.
- State — changes behavior by changing the object’s state representation.
- Strategy — swaps one algorithm for another behind a common interface.
- Template Method — defines an algorithm skeleton while allowing selected steps to vary.
- Visitor — adds operations over a stable object structure without changing every element class.
For example, an order can publish a PaymentApproved notification, use an Observer for interested handlers, and a Strategy for email or SMS delivery. A Facade can simplify the API used by a request handler, while an Adapter can keep a legacy payment client behind the new gateway contract. The combination is useful only when each role is clear.
Tradeoffs
- Use a pattern — reuses a well-understood solution, but adds indirection and requires the team to learn the pattern’s vocabulary.
- Prefer a direct implementation — is faster to read for a small, stable problem, but can duplicate logic when the same pressure appears again.
- Use a catalog decision — makes trade-offs visible, but a catalog does not know your performance, security, or deployment constraints.
- Combine patterns carefully — can express complex variation, but too many collaborators can make execution paths and failure handling difficult to trace.
Patterns are most valuable at a repeated boundary: creating pluggable components, adapting third-party APIs, observing events, or applying a replaceable policy. A one-off method does not need a pattern merely to have one.
When to use
- Creation logic is repeated, expensive, or must choose a compatible family of products.
- A third-party interface must be adapted without changing every caller.
- A subsystem needs a simpler public entry point or a replaceable implementation boundary.
- Several objects must react to a change, and the reaction list should remain open.
- An algorithm varies independently of the object that uses it.
Alternatives
- Direct function calls — fewer layers and easier tracing, but less flexibility when implementations must change independently.
- Event bus or framework container — centralizes wiring, but introduces runtime discovery, ordering, and debugging concerns.
- Plain data plus service functions — reduces object ceremony for simple workflows, but can scatter lifecycle and invariant rules.
- Composition of small interfaces — often a better default than a deep hierarchy, though it still benefits from a named pattern when the structure is recognizable.