Design Patterns Overview
Design Patterns Overview
Definition: Reusable solutions to commonly occurring software design problems, catalogued and organized into three families: Creational, Structural, and Behavioral patterns.
How It Works
- Creational Patterns deal with object instantiation: Factory Method (a method that creates objects without exposing the exact class being created), Builder (constructs a complex object step by step), Singleton (ensures a class has exactly one instance), Abstract Factory (creates families of related objects together).
- Structural Patterns deal with assembling classes and objects into larger structures: Adapter (converts one interface into another a client expects), Decorator (adds behavior to an object dynamically without subclassing), Proxy (stands in for another object to control access to it), Facade (provides a simplified interface to a complex subsystem).
- Behavioral Patterns deal with communication and responsibility assignment between objects: Observer (notifies dependents automatically when an object’s state changes), Strategy (makes an algorithm swappable at runtime), State (lets an object change behavior when its internal state changes), Command (turns a request into a standalone object).
- The catalog originates from the 1994 “Gang of Four” book (Gamma, Helm, Johnson, Vlissides), which formalized 23 patterns and gave the field a shared, precise vocabulary that still dominates how developers talk about object-oriented design today.
- A pattern is not a specific piece of code, it’s a named, reusable shape of a solution, the actual implementation varies by language and context, some patterns (like Singleton) are barely needed in languages with module-level state, since the language already provides an equivalent.
- Patterns often combine: a Factory Method might internally use a Singleton, a Decorator might wrap objects created by an Abstract Factory, real systems compose multiple named patterns together rather than using exactly one in isolation.
- Knowing the catalog changes how you read code, not just how you write it, recognizing “this is a Strategy pattern” instantly tells you the algorithm is meant to be swappable, without needing to trace every call site.
- Beyond the original 23 GoF patterns, the field has accumulated other well-known catalogs: architectural patterns (MVC, MVVM, microservices), concurrency patterns (Producer-Consumer, Thread Pool), and enterprise patterns (Repository, Unit of Work), each addressing problems at a different scale than the original object-oriented catalog.
Under the Hood
Structural relationship: client code depends on an interface, not a concrete class:
Client -> depends on -> Interface (Strategy/Adapter/Factory output)
Interface <- implemented by <- ConcreteClassA, ConcreteClassB, ...
Given: a checkout system that needs to support Stripe, PayPal, and bank transfer payments, chosen at runtime based on the user’s selection.
Step: apply the Strategy pattern: define a PaymentStrategy interface with a pay(amount) method, implement one class per payment provider, inject the chosen strategy into the checkout flow.
Answer: CheckoutService calls strategy.pay(amount) without knowing or caring which concrete class it’s talking to, adding Apple Pay later means adding one new class implementing PaymentStrategy, zero changes to CheckoutService itself.
Given: a legacy LegacyLogger class with a writeLog(msg: string) method, but a new library expects any logger passed to it to implement a log(level, message) interface instead.
Step: apply the Adapter pattern: write a small LegacyLoggerAdapter class implementing the new log(level, message) interface, internally calling legacyLogger.writeLog(...).
Answer: the new library works with the old logger unchanged, without modifying either the legacy class or the new library’s expected interface, the adapter is the only new code needed to bridge the mismatch.
Given: a notification system where email, SMS, and push notification handlers all need to run whenever a UserSignedUp event happens, and new handlers get added over time.
Step: apply the Observer pattern: UserSignedUp becomes a subject that handlers subscribe to, each handler implements a common notify(event) method.
Answer: the signup code fires one event, subject.notifyAll(event), and every subscribed handler runs independently, adding a Slack notification handler later means subscribing a new class, zero changes to the signup code path itself.
Given: a UI library building complex dialog objects with optional title, optional icon, required message, and optional list of buttons, where most call sites only set 2-3 of these fields.
Step: apply the Builder pattern: new DialogBuilder().setMessage("Confirm?").addButton("OK").build(), instead of a constructor with 6 optional parameters most callers pass as null.
Answer: call sites read clearly with only the fields they care about set, avoiding both a combinatorial explosion of constructor overloads and the “what does the 4th null parameter mean” readability problem of a long positional constructor.
Why It Matters
- Provides standardized, shared terminology and battle-tested architectural blueprints, “let’s use an Observer here” communicates a precise design intent in three words that would otherwise take a paragraph to explain.
- Patterns encode decades of accumulated experience about what tends to go wrong in specific design situations, using one is often faster and safer than reinventing an ad hoc solution to the same recurring problem.
- Understanding the catalog makes reading unfamiliar codebases faster, recognizing a pattern’s shape lets you predict how the surrounding code likely behaves before reading every line.
- Patterns show up constantly in the libraries and frameworks developers use daily (Observer in event listeners, Factory in dependency injection containers, Decorator in middleware chains), fluency with the vocabulary is directly practical, not just academic.
- Interview processes at many companies explicitly test pattern knowledge, not to see if a candidate memorized the GoF book, but because recognizing when to reach for Strategy versus State versus a plain conditional is a genuine signal of design judgment.
Common Pitfalls
- Forcing design patterns into problems where a simple function would be clearer, sometimes called “patternitis,” adding a Factory and a Strategy interface for a function that will only ever have one implementation is needless indirection.
- Treating pattern names as a checklist to use rather than a vocabulary to recognize, a good design sometimes has zero named patterns in it, patterns describe recurring shapes, they aren’t a requirement to hit a quota of.
- Misapplying Singleton for global state that should really be passed explicitly as a dependency, Singletons make testing harder (hidden shared state between tests) and are frequently overused where dependency injection would be cleaner.
- Confusing structurally similar patterns, Strategy and State look almost identical in code (both swap behavior via an interface), the difference is intent: Strategy is chosen externally by a client, State transitions itself based on internal conditions.
- Over-engineering an Abstract Factory or Builder for an object with two constructor parameters, these patterns pay off for genuinely complex construction logic, not for objects a plain constructor already handles cleanly.
- Assuming patterns are language-agnostic in implementation detail as well as intent, a pattern that needs verbose boilerplate in Java might be a single built-in language feature in Python or JavaScript (a closure often replaces a full Strategy class hierarchy).
- Naming a class after a pattern without it actually behaving like that pattern, calling something
UserFactorywhen it’s really just a constructor wrapper adds confusion instead of clarity.
Comparison
| Creational | Structural | Behavioral | |
|---|---|---|---|
| Concerned with | How objects get created | How objects and classes are composed | How objects communicate and share responsibility |
| Example patterns | Factory Method, Builder, Singleton | Adapter, Decorator, Facade, Proxy | Observer, Strategy, State, Command |
| Typical problem solved | Decoupling client code from concrete construction logic | Making incompatible or overly complex interfaces easier to work with | Making interaction between objects flexible and swappable |
| Common real-world use | Dependency injection containers | Middleware, ORMs, wrapper libraries | Event systems, plugin architectures |
Pattern Quick Reference
| Pattern | Family | One-line purpose |
|---|---|---|
| Factory Method | Creational | Delegate object creation to a method, not a direct constructor call |
| Builder | Creational | Construct a complex object step by step |
| Singleton | Creational | Guarantee exactly one instance exists |
| Adapter | Structural | Make an incompatible interface usable |
| Decorator | Structural | Add behavior to an object without subclassing |
| Facade | Structural | Simplify a complex subsystem behind one interface |
| Observer | Behavioral | Notify dependents automatically on state change |
| Strategy | Behavioral | Swap an algorithm at runtime |
| Command | Behavioral | Turn a request into a standalone, queueable object |
| State | Behavioral | Change behavior automatically as internal state changes |
| Proxy | Structural | Control or defer access to another object |
| Abstract Factory | Creational | Create families of related objects without specifying concrete classes |
| Composite | Structural | Treat individual objects and groups of objects uniformly |
Example
Using the Strategy pattern to switch sorting algorithms at runtime, a SortContext holding a reference to any class implementing a sort(array) method, lets client code change from quicksort to mergesort without touching the code that calls sort.
React and Vue’s component-based rendering, and the DOM’s own addEventListener, are both implementations of the Observer pattern: components (or listeners) subscribe to events and get notified automatically when the underlying state or event fires, without the source needing to know who’s listening.
Express.js and most other web frameworks implement middleware chains using the Decorator pattern, each middleware function wraps the request handler with additional behavior (logging, auth checks, body parsing) without modifying the original handler’s code.
Dependency injection frameworks like Spring (Java) and Angular’s built-in DI container are essentially industrial-strength implementations of the Factory and Singleton patterns combined, managing object creation and lifetime so application code never calls new directly for its dependencies.
Related Terms
Referenced by