Introduction
Design patterns are repeatable, time-tested solutions that address common problems encountered when designing software. This expanded guide explains many widely used patterns, grouped by intent, with practical examples and concise pseudocode to help you apply them appropriately.
Why design patterns matter
Patterns give teams a shared vocabulary and a set of trade-offs for solving recurring design problems. They reduce the need to reinvent solutions, make intent explicit, and can guide refactors toward more maintainable designs.
How to use this guide
Read the pattern intent, consider the typical use cases, and study the short code snippets. Use these as starting points — adapt the pattern to your language and constraints rather than applying it verbatim.
Creational patterns
Singleton
Intent: Ensure a class has only one instance and provide a global access point.
When to use: Shared configuration, logging, or connection managers where a single coordinated instance is required. Prefer dependency injection when testability and explicit dependencies matter — singletons can hide dependencies and make unit testing harder.
Notes: Consider lifecycle (creation time), thread-safety in concurrent environments, and whether a module-level object (in languages like JavaScript or Python) already provides the needed behavior.
// singleton (pseudocode) - lazy, simple
class Config {
private static instance;
private constructor(){ /* load defaults */ }
static getInstance(){
if(!Config.instance) Config.instance = new Config();
return Config.instance;
}
}
// example (JS module style) - easier to test if exported separately
// config.js
const config = { env: process.env.NODE_ENV || 'production' };
module.exports = config; // module singleton
Factory Method
Intent: Define an interface for creating objects; defer instantiation to subclasses so callers depend on abstractions rather than concrete classes.
When to use: When a class cannot anticipate the concrete types it must create, or when you want to centralize creation logic (for configuration, caching or lifecycle control).
// factory method (pseudocode)
interface Product {}
class ConcreteProduct implements Product {}
class Creator {
// subclasses override create() to provide different products
create(){ return new ConcreteProduct(); }
}
// example: choose implementation at runtime
class CreatorA extends Creator { create(){ return new ProductA(); } }
class CreatorB extends Creator { create(){ return new ProductB(); } }
Abstract Factory
Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
// abstract factory (pseudocode)
interface WidgetFactory { createButton(); createMenu(); }
class MacFactory implements WidgetFactory { createButton(){/* mac */} createMenu(){/* mac */} }
class WinFactory implements WidgetFactory { createButton(){/* win */} createMenu(){/* win */} }
Builder
Intent: Construct complex objects step by step and allow different representations.
// builder (pseudocode)
class CarBuilder { setEngine(e){} setWheels(n){} build(){ return new Car(...) } }
const car = new CarBuilder().setEngine('V6').setWheels(4).build();
Prototype
Intent: Create new objects by copying a prototypical instance, useful when object creation is expensive.
// prototype (pseudocode)
const prototype = { a:1, b:2 };
function clone(obj){ return JSON.parse(JSON.stringify(obj)); }
const copy = clone(prototype);
Object Pool
Intent: Reuse a set of initialized objects instead of creating/destroying them on demand. Useful for expensive-to-create resources (DB connections, worker threads, expensive DOM nodes).
Considerations: set pool size limits, validate objects on checkout/return, and handle timeouts to avoid resource starvation.
// object pool (pseudocode)
class Pool {
constructor(create, max = 10){ this.create = create; this.free = []; this.max = max; this.active = 0; }
acquire(){
if(this.free.length) return this.free.pop();
if(this.active < this.max){ this.active++; return this.create(); }
throw new Error('No available resource');
}
release(obj){ this.free.push(obj); }
}
// example: connection pool
const pool = new Pool(() => openDbConnection(), 20);
try { const conn = pool.acquire(); /* use */ pool.release(conn); } catch(e){ /* retry or fail */ }
Structural patterns
Adapter
Intent: Make incompatible interfaces work together by translating calls.
Simple: an Adapter is a translator — it converts one interface into another so two parts can work together without changing either one.
Analogy: like an electrical plug adapter that lets a device from one country use the sockets of another country.
// adapter (pseudocode)
class OldApi { fetchData(){ return { 'old': true }; } }
class ApiAdapter { constructor(old){ this.old = old; } fetch(){ const r = this.old.fetchData(); return { newFormat: r.old }; } }
Bridge
Intent: Decouple an abstraction from its implementation so both can vary independently.
// bridge (pseudocode)
class Renderer { renderCircle(r){} }
class VectorRenderer extends Renderer {}
class RasterRenderer extends Renderer {}
class Shape { constructor(renderer){ this.renderer = renderer; } }
class Circle extends Shape { draw(){ this.renderer.renderCircle(this.radius); } }
Composite
Intent: Compose objects into tree structures to represent part-whole hierarchies; treat components uniformly.
Simple: Composite lets you treat single objects and collections of objects the same way — useful when items and groups behave similarly.
Analogy: a folder in a filesystem can contain files or other folders, and you can ask any folder to "list contents" the same way.
// composite (pseudocode)
class Component { render(){} }
class Leaf extends Component { render(){ /* single */ } }
class Composite extends Component { constructor(){ this.children=[] } add(c){ this.children.push(c) } render(){ this.children.forEach(c=>c.render()); } }
Decorator
Intent: Add behavior to objects dynamically without changing their interface.
Simple: wrap an object with extra features at runtime — the original API stays the same but behavior is extended.
Analogy: putting a protective case on a phone that also adds a kickstand — the phone is the same, now with extra features attached.
// decorator (pseudocode)
function withLogging(service){
return function(...args){
console.log('call', { args });
const result = service.apply(this, args);
console.log('result', result);
return result;
};
}
// example: add timing
function withTiming(service){
return function(...args){ const t0 = Date.now(); const r = service(...args); console.log('ms', Date.now()-t0); return r; };
}
Facade
Intent: Provide a simplified interface to a complex subsystem.
// facade (pseudocode)
class VideoConverterFacade { convert(file, format){ /* uses many subsystems */ } }
Flyweight
Intent: Share common parts of state between many objects to reduce memory usage.
// flyweight (pseudocode)
const glyphFactory = {};
function getGlyph(char){ if(!glyphFactory[char]) glyphFactory[char] = { char }; return glyphFactory[char]; }
Proxy
Intent: Provide a surrogate or placeholder for another object to control access.
Use cases: lazy initialization, access control, caching, remote stubs. Proxies intercept calls and can add layering (caching, retries, logging) while preserving the target's API.
Simple: a Proxy sits between the caller and the real object and can add checks, caching, or delays before delegating to the real object.
Analogy: a receptionist who filters visitors — they check identity and either forward the visitor or block them.
// proxy (pseudocode)
class RemoteServiceProxy {
constructor(real){ this.real = real; this.cache = new Map(); }
async request(key){
if(this.cache.has(key)) return this.cache.get(key);
const res = await this.real.request(key);
this.cache.set(key, res);
return res;
}
}
Behavioral patterns
Chain of Responsibility
Intent: Pass a request along a chain of handlers until one handles it.
// chain (pseudocode)
class Handler { constructor(next){ this.next = next } handle(req){ if(this.canHandle(req)) return this.process(req); if(this.next) return this.next.handle(req); }
}
Command
Intent: Encapsulate a request as an object, allowing parameterization and queuing.
// command (pseudocode)
class Command { execute(){} }
class SaveCommand extends Command { execute(){ db.save(this.data); } }
queue.push(new SaveCommand(data));
Interpreter
Intent: Define a representation for a language's grammar along with an interpreter to process sentences in the language.
// simple interpreter (pseudocode)
// parse and evaluate expressions like 'add 2 3'
Iterator
Intent: Provide a way to access elements of an aggregate object sequentially without exposing its underlying representation.
// iterator (pseudocode)
for (let it = collection.iterator(); it.hasNext(); ) { let item = it.next(); }
Mediator
Intent: Define an object that encapsulates how a set of objects interact, reducing direct dependencies between them.
// mediator (pseudocode)
class ChatRoom { send(from, to, msg){ /* find participant and deliver */ } }
Memento
Intent: Capture and externalize an object's internal state so it can be restored later without violating encapsulation.
// memento (pseudocode)
const snapshot = originator.save(); originator.restore(snapshot);
Observer
Intent: Define a one-to-many dependency so that when one object changes state, its dependents are notified.
Notes: Observers are great for decoupling producers and consumers (UI updates, event systems). Watch for memory leaks: remove subscriptions when observers are no longer needed.
Simple: one subject, many observers — when the subject changes, it tells all observers so they can react.
Analogy: subscribing to a newsletter — when the author publishes, every subscriber receives the update.
// observer (pseudocode)
class Subject {
constructor(){ this.observers = new Set(); }
subscribe(o){ this.observers.add(o); }
unsubscribe(o){ this.observers.delete(o); }
notify(data){ this.observers.forEach(o => o.update && o.update(data)); }
}
// example: small JS event pattern
const subject = new Subject();
const observer = { update: d => console.log('got', d) };
subject.subscribe(observer);
subject.notify({ value: 42 });
State
Intent: Allow an object to alter its behavior when its internal state changes.
// state (pseudocode)
class State { handle(ctx){} }
class Context { setState(s){ this.state = s } request(){ this.state.handle(this); } }
Strategy
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable.
When to use: When you need to support multiple algorithms or policies (sorting, validation, routing) and switch them at runtime without branching logic scattered across callers.
Simple: Strategy lets you swap the algorithm an object uses without changing the object itself — pass different strategies as needed.
Analogy: choosing a payment method at checkout — the process is the same, but the payment algorithm varies (card, PayPal, bank transfer).
// strategy (pseudocode)
function process(items, sorter){ return sorter.sort(items); }
// example: swapping sorting strategies
const asc = { sort: arr => arr.slice().sort((a,b)=>a-b) };
const desc = { sort: arr => arr.slice().sort((a,b)=>b-a) };
process([3,1,2], asc); // [1,2,3]
Template Method
Intent: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses.
// template method (pseudocode)
class DataProcessor { process(){ this.read(); this.transform(); this.write(); } }
Visitor
Intent: Represent an operation to be performed on elements of an object structure without changing the classes on which it operates.
// visitor (pseudocode)
node.accept(visitor);
Null Object
Intent: Provide an object with a neutral behavior to avoid null checks.
// null object (pseudocode)
class NullLogger { log(){} }
const logger = config.logger || new NullLogger();
Architectural and higher-level patterns
MVC / MVP / MVVM
Intent: Separate concerns between data (model), UI (view), and presentation or coordination logic (controller/presenter/view-model).
These patterns improve testability and maintainability for user-facing applications.
Event Sourcing & CQRS (overview)
Intent: Persist state changes as a sequence of events (Event Sourcing) and separate read/write models (CQRS) to optimize scalability and auditing.
Practical example: Plugin host (expanded)
A plugin host can combine Observer (event dispatch), Strategy (plugin-provided behavior), and Adapter (compatibility wrappers). Plugins subscribe to events and register strategies to extend application behavior without direct coupling.
// plugin registration (pseudocode)
const host = new Host();
host.on('open', plugin.handleOpen.bind(plugin));
host.registerStrategy('export', plugin.exportStrategy);
Practical example: Resource pool (expanded)
Combine Factory (object creation), Object Pool (reuse), and Singleton (single pool manager) to manage expensive resources such as DB connections.
Real-world implementations (brief)
JavaScript — Observer (EventEmitter)
Quick example using Node.js-style events to implement an Observer.
// Node.js style observer
const EventEmitter = require('events');
class Bus extends EventEmitter {}
const bus = new Bus();
bus.on('data', d => console.log('received', d));
bus.emit('data', { id: 1 });
Python — Strategy (callable classes)
Use simple classes or callables to swap algorithms at runtime.
# python strategy
class SortStrategy:
def sort(self, items): raise NotImplementedError
class QuickSort(SortStrategy):
def sort(self, items): return sorted(items)
def process(items, strategy): return strategy.sort(items)
Java — Factory
Factory methods decouple construction from usage and simplify testing.
// java factory (simplified)
interface Product {}
class ConcreteProduct implements Product {}
class ProductFactory { static Product create(){ return new ConcreteProduct(); } }
Quick reference: When to choose a pattern
- Singleton: single shared resource (prefer DI when possible)
- Factory / Abstract Factory: decouple creation from usage
- Strategy: swap algorithms at runtime
- Observer: pub/sub or event-driven updates
- Decorator / Proxy: add behavior without changing callers
- Adapter: integrate incompatible interfaces
Further reading & resources
Recommended: "Design Patterns: Elements of Reusable Object-Oriented Software" (Gamma et al.), online pattern catalogs, and curated example repositories on GitHub.
Anti-patterns and common mistakes
Applying patterns prematurely or forcing a pattern where a simple solution suffices can increase complexity. Prefer YAGNI (You Aren't Gonna Need It) and refactor toward patterns when real needs appear.
Testing and maintainability
Many patterns improve testability by isolating responsibilities and enabling dependency injection. For example, Strategy and Factory make it easy to substitute test doubles.
Performance considerations
Some patterns add indirection (Proxy, Decorator, Bridge) or shared state (Flyweight). Always measure and consider trade-offs between clarity and runtime/memory cost.
Checklist for applying a pattern
- Is there a recurring design problem the pattern addresses?
- Does it reduce duplication or clarify responsibilities?
- Can you implement it incrementally and keep tests green?
- Will added indirection be acceptable for maintenance and performance?
Further reading and learning approach
Start with a handful of patterns (Factory, Strategy, Observer, Singleton, Adapter) and apply them in small refactors. Learn to recognize the problem types patterns solve and avoid memorizing syntax — focus on intent and trade-offs.
Conclusion
Design patterns are practical building blocks for reliable software. Use them judiciously, test thoroughly, and prefer clarity over cleverness. Try refactoring one small area in your codebase using a pattern from this guide.
Action: Pick one pattern from this article and apply it to a small module in your project — observe the benefits and trade-offs.