Design Patterns

16 classic software design patterns explained in plain English — with real-world analogies, TypeScript code examples, and guidance on when (and when not) to use each one.

Creational Patterns

Control object creation — when and how objects are instantiated.

Singleton

Creational

Ensure only one instance of a class exists, and provide a global access point to it.

🎯 Real-world analogy

Like the President of a country — there is only one at a time, and everyone contacts the same person.

✅ When to use

Global app configuration, logging service, database connection pool, cache manager.

📋 Code example (TypeScript)
// Node.js module-level singleton (modules are cached after first require)
class Logger {
  private static instance: Logger;
  private constructor() {}

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  log(msg: string) { console.log(`[LOG] ${msg}`); }
}

const logger = Logger.getInstance();
Watch out: Singletons make unit testing harder because they carry shared state between tests. Prefer dependency injection where possible.

Factory Method

Creational

Define an interface for creating an object, but let subclasses decide which class to instantiate.

🎯 Real-world analogy

Like a logistics company: you call "send a package" without specifying whether it goes by truck, plane, or ship — the factory decides based on the destination.

✅ When to use

When you don't know at compile time what class you need. Plugin systems, UI component creation, parser selection by file type.

📋 Code example (TypeScript)
interface Notifier {
  send(message: string): void;
}

class EmailNotifier implements Notifier {
  send(message: string) { /* send email */ }
}

class SMSNotifier implements Notifier {
  send(message: string) { /* send SMS */ }
}

// Factory decides which to create
function createNotifier(type: 'email' | 'sms'): Notifier {
  if (type === 'email') return new EmailNotifier();
  return new SMSNotifier();
}
Watch out: Can lead to class proliferation — too many small subclasses for each product type.

Abstract Factory

Creational

Provide an interface for creating families of related objects without specifying their concrete classes.

🎯 Real-world analogy

Like an IKEA vs. a designer furniture store — each "factory" produces a whole family of matching furniture (table, chair, shelf), all in the same style.

✅ When to use

Creating UI components that must match a theme/platform (Windows vs. Mac), or cross-platform database clients (PostgreSQL family vs. MySQL family).

📋 Code example (TypeScript)
interface Button { render(): void; }
interface Input  { render(): void; }

// Two product families
class WindowsButton implements Button { render() { console.log('Windows button'); } }
class MacButton     implements Button { render() { console.log('Mac button');     } }
class WindowsInput  implements Input  { render() { console.log('Windows input');  } }
class MacInput      implements Input  { render() { console.log('Mac input');      } }

interface UIFactory { createButton(): Button; createInput(): Input; }

class WindowsFactory implements UIFactory {
  createButton() { return new WindowsButton(); }
  createInput()  { return new WindowsInput();  }
}
Watch out: Adding a new product type (e.g. a Checkbox) requires changing every factory class — can be expensive.

Builder

Creational

Separate the construction of a complex object from its representation, allowing the same process to create different types.

🎯 Real-world analogy

Like a restaurant order builder: you add items one by one (burger, side, drink) and call "finish order" when done — you don't construct the entire meal object in one shot.

✅ When to use

Objects with many optional fields (avoid telescoping constructors), query builders, HTTP request builders, test data factories.

📋 Code example (TypeScript)
class QueryBuilder {
  private table = '';
  private conditions: string[] = [];
  private limitVal = 100;

  from(table: string)       { this.table = table; return this; }
  where(cond: string)       { this.conditions.push(cond); return this; }
  limit(n: number)          { this.limitVal = n; return this; }
  build(): string {
    const where = this.conditions.length
      ? `WHERE ${this.conditions.join(' AND ')}` : '';
    return `SELECT * FROM ${this.table} ${where} LIMIT ${this.limitVal}`; 
  }
}

const sql = new QueryBuilder()
  .from('users')
  .where('active = true')
  .limit(20)
  .build();
Watch out: Adds boilerplate — overkill for simple objects with 2–3 fields.

Prototype

Creational

Create new objects by copying (cloning) an existing object instead of constructing from scratch.

🎯 Real-world analogy

Like a photocopier: you copy an existing document and then make small edits, rather than typing the whole document again.

✅ When to use

When object creation is expensive (database reads, complex initialisation) and cloning is cheaper. Clone templates to create slight variations.

📋 Code example (TypeScript)
class Config {
  constructor(
    public host: string,
    public port: number,
    public debug: boolean
  ) {}

  clone(): Config {
    return new Config(this.host, this.port, this.debug);
  }
}

const base = new Config('localhost', 5432, false);
const devConfig = base.clone();
devConfig.debug = true;   // only change what's different
Watch out: Deep vs. shallow copy: if the object contains references to mutable objects, a shallow clone means both share the same reference. Use deep cloning where needed.

Structural Patterns

Organise classes and objects into larger structures.

Adapter

Structural

Convert the interface of a class into another interface that clients expect — making incompatible interfaces work together.

🎯 Real-world analogy

Like a travel power adapter: the device has one plug shape, the wall socket has another — the adapter makes them compatible without changing either.

✅ When to use

Integrating a legacy system or third-party library with a different interface. Wrapping an old API to match a new contract.

📋 Code example (TypeScript)
// Old payment service interface
class OldPaymentService {
  initiateTransaction(amount: number, currency: string) { /* ... */ }
}

// New interface your app expects
interface PaymentProcessor {
  pay(amountCents: number): void;
}

// Adapter wraps the old service to match the new interface
class PaymentAdapter implements PaymentProcessor {
  constructor(private legacy: OldPaymentService) {}
  pay(amountCents: number) {
    this.legacy.initiateTransaction(amountCents / 100, 'USD');
  }
}
Watch out: Too many adapters can indicate your system has structural issues. Prefer a clean architecture where possible.

Decorator

Structural

Attach additional responsibilities to an object dynamically — a flexible alternative to subclassing for extending behaviour.

🎯 Real-world analogy

Like adding toppings to a pizza: you start with a base and add cheese, then mushrooms, then pepperoni — each topping wraps the previous one without modifying the pizza base.

✅ When to use

Adding cross-cutting concerns (logging, caching, authentication, compression) without modifying the original class.

📋 Code example (TypeScript)
interface DataService {
  fetchUser(id: string): Promise<User>;
}

class UserService implements DataService {
  async fetchUser(id: string) { /* DB call */ return user; }
}

// Decorator adds caching on top
class CachedUserService implements DataService {
  private cache = new Map<string, User>();
  constructor(private inner: DataService) {}

  async fetchUser(id: string) {
    if (this.cache.has(id)) return this.cache.get(id)!;
    const user = await this.inner.fetchUser(id);
    this.cache.set(id, user);
    return user;
  }
}

const service = new CachedUserService(new UserService());
Watch out: Stacking many decorators creates "decorator hell" — hard to debug and trace the call chain.

Facade

Structural

Provide a simplified interface to a complex subsystem — hide internal complexity behind a single, clean surface.

🎯 Real-world analogy

Like the reception desk at an office building: you talk to one person (the facade), who internally coordinates security, lifts, visitor badges, and room booking.

✅ When to use

Simplifying access to a complex library or framework. Providing a clean API over multiple service calls. SDK design.

📋 Code example (TypeScript)
// Complex subsystem
class VideoEncoder    { encode(file: string)  { /* ... */ } }
class AudioExtractor  { extract(file: string) { /* ... */ } }
class ThumbnailGen    { generate(file: string){ /* ... */ } }
class CDNUploader     { upload(file: string)  { /* ... */ } }

// Simple facade
class VideoProcessingFacade {
  private encoder   = new VideoEncoder();
  private audio     = new AudioExtractor();
  private thumbnail = new ThumbnailGen();
  private uploader  = new CDNUploader();

  processAndUpload(file: string) {
    this.encoder.encode(file);
    this.audio.extract(file);
    this.thumbnail.generate(file);
    this.uploader.upload(file);
  }
}
Watch out: A façade can become a "god object" if it accumulates too many responsibilities instead of delegating.

Proxy

Structural

Provide a surrogate that controls access to another object — add lazy initialisation, access control, logging, or caching transparently.

🎯 Real-world analogy

Like a credit card: it's a proxy for your bank account. You use the card (proxy) everywhere; it handles the actual bank transaction on your behalf, with access controls built in.

✅ When to use

Remote proxies (stub for network service), virtual proxies (lazy load large objects), protection proxies (ACL checks), caching proxies.

📋 Code example (TypeScript)
interface DatabaseService {
  query(sql: string): any[];
}

class RealDatabase implements DatabaseService {
  query(sql: string) { /* expensive DB call */ return []; }
}

// Access-control proxy
class SecureDatabaseProxy implements DatabaseService {
  constructor(
    private db: RealDatabase,
    private user: { role: string }
  ) {}

  query(sql: string) {
    if (this.user.role !== 'admin' && sql.includes('DROP')) {
      throw new Error('Permission denied');
    }
    return this.db.query(sql);
  }
}
Watch out: Proxies add indirection — debugging can be tricky when the proxy silently intercepts calls.

Composite

Structural

Compose objects into tree structures to represent part–whole hierarchies, letting clients treat individual objects and compositions uniformly.

🎯 Real-world analogy

Like a file system: a folder contains files and other folders. You can call "get size" on a single file or on an entire folder — the interface is the same.

✅ When to use

Tree-structured data: organisation charts, file systems, UI component trees, menu hierarchies, permission groups.

📋 Code example (TypeScript)
interface FileSystemNode {
  getSize(): number;
  getName(): string;
}

class File implements FileSystemNode {
  constructor(private name: string, private size: number) {}
  getSize() { return this.size; }
  getName() { return this.name; }
}

class Folder implements FileSystemNode {
  private children: FileSystemNode[] = [];
  constructor(private name: string) {}
  add(node: FileSystemNode) { this.children.push(node); }
  getSize() { return this.children.reduce((sum, c) => sum + c.getSize(), 0); }
  getName() { return this.name; }
}

const root = new Folder('src');
root.add(new File('index.ts', 1200));
root.add(new File('utils.ts', 800));
console.log(root.getSize()); // 2000
Watch out: Can make it hard to restrict what types go inside a composite — any FileSystemNode can be added to any Folder.

Behavioral Patterns

Define how objects communicate and distribute responsibility.

Observer

Behavioral

Define a one-to-many dependency so that when one object changes state, all its dependents are notified and updated automatically.

🎯 Real-world analogy

Like a newsletter: you subscribe, and whenever new content is published, all subscribers receive it — the publisher doesn't need to know who they are.

✅ When to use

Event systems, reactive state management (React's useState, RxJS), webhook notifications, real-time UI updates, pub/sub messaging.

📋 Code example (TypeScript)
type Listener<T> = (data: T) => void;

class EventEmitter<T> {
  private listeners: Listener<T>[] = [];
  subscribe(fn: Listener<T>)   { this.listeners.push(fn); }
  unsubscribe(fn: Listener<T>) { this.listeners = this.listeners.filter(l => l !== fn); }
  emit(data: T)                { this.listeners.forEach(l => l(data)); }
}

const authEvents = new EventEmitter<{ userId: string; event: string }>();

authEvents.subscribe(({ userId, event }) => {
  console.log(`[AUDIT] ${userId}: ${event}`);
});

authEvents.emit({ userId: 'u-123', event: 'LOGIN' });
Watch out: Subscribers that are not unsubscribed when their lifecycle ends cause memory leaks. Always clean up in component unmount / destructor.

Strategy

Behavioral

Define a family of algorithms, encapsulate each one, and make them interchangeable — the algorithm varies independently from clients that use it.

🎯 Real-world analogy

Like a GPS app: you choose your transport mode (driving, cycling, walking) and the routing algorithm changes accordingly — the app interface stays the same.

✅ When to use

Sorting algorithms, payment methods, compression strategies, authentication mechanisms, export formats (PDF / Excel / CSV).

📋 Code example (TypeScript)
interface SortStrategy {
  sort(data: number[]): number[];
}

class QuickSort implements SortStrategy {
  sort(data: number[]) { return [...data].sort((a, b) => a - b); }
}

class BubbleSort implements SortStrategy {
  sort(data: number[]) {
    const arr = [...data];
    for (let i = 0; i < arr.length; i++)
      for (let j = 0; j < arr.length - i - 1; j++)
        if (arr[j] > arr[j+1]) [arr[j], arr[j+1]] = [arr[j+1], arr[j]];
    return arr;
  }
}

class Sorter {
  constructor(private strategy: SortStrategy) {}
  setStrategy(s: SortStrategy) { this.strategy = s; }
  sort(data: number[]) { return this.strategy.sort(data); }
}
Watch out: Clients must be aware of different strategies to select the right one — can expose implementation details.

Command

Behavioral

Encapsulate a request as an object, letting you parameterise methods, queue requests, log them, and support undoable operations.

🎯 Real-world analogy

Like a restaurant order slip: the waiter writes the order, hands it to the kitchen — the order can be queued, cancelled, or recorded as history. The waiter doesn't cook.

✅ When to use

Undo/redo functionality, task queues, macros, transactional operations, audit logging, GUI action buttons.

📋 Code example (TypeScript)
interface Command { execute(): void; undo(): void; }

class AddItemCommand implements Command {
  constructor(
    private cart: string[],
    private item: string
  ) {}
  execute() { this.cart.push(this.item); }
  undo()    { this.cart.pop(); }
}

class CommandHistory {
  private history: Command[] = [];
  run(cmd: Command) { cmd.execute(); this.history.push(cmd); }
  undo() { this.history.pop()?.undo(); }
}

const cart: string[] = [];
const history = new CommandHistory();
history.run(new AddItemCommand(cart, 'Book'));
console.log(cart);  // ['Book']
history.undo();
console.log(cart);  // []
Watch out: Every new operation needs a new Command class — can create many small classes.

Iterator

Behavioral

Provide a way to access elements of a collection sequentially without exposing the underlying data structure.

🎯 Real-world analogy

Like a TV remote's "next channel" button: you navigate the channel list sequentially without knowing that it's stored as an array, a linked list, or something else.

✅ When to use

Traversing custom data structures (trees, graphs, DB cursors, file streams) without exposing their internals.

📋 Code example (TypeScript)
// JavaScript/TypeScript has built-in iterator protocol
class Range {
  constructor(private start: number, private end: number) {}

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next(): IteratorResult<number> {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: 0, done: true };
      }
    };
  }
}

for (const n of new Range(1, 5)) {
  console.log(n);  // 1, 2, 3, 4, 5
}
Watch out: Custom iterators add complexity — prefer built-in language iteration (for...of, generators) when possible.

Chain of Responsibility

Behavioral

Pass a request along a chain of handlers, each deciding to process it or pass it to the next handler in the chain.

🎯 Real-world analogy

Like customer support escalation: your issue goes to tier-1 support → if unresolved, to tier-2 → then to engineering. Each level handles what it can and escalates the rest.

✅ When to use

HTTP middleware (Express, Koa), validation pipelines, event propagation, permission checks, log level filtering.

📋 Code example (TypeScript)
abstract class Handler {
  protected next?: Handler;
  setNext(h: Handler) { this.next = h; return h; }
  abstract handle(request: number): string | null;
}

class LowPriorityHandler extends Handler {
  handle(req: number) {
    if (req < 10) return `Low handles ${req}`;
    return this.next?.handle(req) ?? null;
  }
}

class HighPriorityHandler extends Handler {
  handle(req: number) {
    if (req >= 10) return `High handles ${req}`;
    return this.next?.handle(req) ?? null;
  }
}

const low  = new LowPriorityHandler();
const high = new HighPriorityHandler();
low.setNext(high);

console.log(low.handle(5));   // "Low handles 5"
console.log(low.handle(15));  // "High handles 15"
Watch out: A request may fall through the whole chain without being handled — always handle the fallthrough case.

Template Method

Behavioral

Define the skeleton of an algorithm in a base class, deferring some steps to subclasses — subclasses override specific steps without changing the algorithm's structure.

🎯 Real-world analogy

Like a standardised interview process: every candidate goes through phone screen → technical test → panel interview, but the specific questions vary by role.

✅ When to use

When multiple classes share the same algorithm structure but differ in specific steps — report generation, data processing pipelines, test framework setup/teardown.

📋 Code example (TypeScript)
abstract class DataMigration {
  // Template method: defines the algorithm structure
  run() {
    this.connect();
    const data = this.extract();
    const transformed = this.transform(data);
    this.load(transformed);
    this.disconnect();
  }

  private connect()    { console.log('Connecting to DB...'); }
  private disconnect() { console.log('Disconnecting.'); }

  // Steps deferred to subclasses
  abstract extract(): any[];
  abstract transform(data: any[]): any[];
  abstract load(data: any[]): void;
}

class UserMigration extends DataMigration {
  extract()            { return [{ id: 1, name: 'Alice' }]; }
  transform(data: any[]){ return data.map(u => ({ ...u, migrated: true })); }
  load(data: any[])    { console.log('Saving', data); }
}
Watch out: Violates the Liskov Substitution Principle if subclasses fundamentally change the algorithm's behaviour. Keep subclass overrides constrained.

Quick Reference

Pattern Category One-line intent Key words
Singleton Creational Ensure only one instance of a class exists, and provide a global access point to it. instance, global access
Factory Method Creational Define an interface for creating an object, but let subclasses decide which class to instantiate. create, subclass, interface
Abstract Factory Creational Provide an interface for creating families of related objects without specifying their concrete classes. family, related objects
Builder Creational Separate the construction of a complex object from its representation, allowing the same process to create different types. step-by-step, complex object
Prototype Creational Create new objects by copying (cloning) an existing object instead of constructing from scratch. clone, copy, template
Adapter Structural Convert the interface of a class into another interface that clients expect wrapper, compatible, legacy
Decorator Structural Attach additional responsibilities to an object dynamically wrap, add behaviour, dynamic
Facade Structural Provide a simplified interface to a complex subsystem simplify, subsystem, API
Proxy Structural Provide a surrogate that controls access to another object surrogate, control, cache
Composite Structural Compose objects into tree structures to represent part–whole hierarchies, letting clients treat individual objects and compositions uniformly. tree, part-whole, hierarchy
Observer Behavioral Define a one-to-many dependency so that when one object changes state, all its dependents are notified and updated automatically. subscribe, notify, event
Strategy Behavioral Define a family of algorithms, encapsulate each one, and make them interchangeable algorithm, interchangeable
Command Behavioral Encapsulate a request as an object, letting you parameterise methods, queue requests, log them, and support undoable operations. undo, queue, encapsulate
Iterator Behavioral Provide a way to access elements of a collection sequentially without exposing the underlying data structure. traverse, sequential
Chain of Responsibility Behavioral Pass a request along a chain of handlers, each deciding to process it or pass it to the next handler in the chain. middleware, escalate
Template Method Behavioral Define the skeleton of an algorithm in a base class, deferring some steps to subclasses skeleton, override, hook