Design Patterns: The Complete Guide to Reusable Software Architecture

Master 23+ time-tested design patterns — Creational, Structural and Behavioral — with full code examples in multiple languages, real-world analogies, when-to-use guidance, anti-pattern warnings and a quick-reference cheat sheet.

1. Introduction

Design patterns are repeatable, well-documented solutions to common problems in software design. Catalogued by the "Gang of Four" (Gamma, Helm, Johnson, Vlissides) in 1994, they remain essential tools for every developer — not as rigid templates, but as a shared vocabulary for discussing trade-offs and structuring code.

This guide covers all 23 GoF patterns plus several modern additions, with full code examples, real-world analogies and clear guidance on when (and when not) to use each one.

2. Why Design Patterns Matter

  • Shared vocabulary — Saying "we need a Strategy here" communicates instantly to the team without lengthy explanations.
  • Proven solutions — Patterns have been battle-tested across millions of projects. They encode decades of collective wisdom.
  • Reduce reinvention — Instead of designing from scratch, start with a known solution and adapt.
  • Guide refactoring — When code smells appear (long switches, duplicated logic, tight coupling), patterns show the direction for improvement.
  • Improve testability — Many patterns (Strategy, Factory, Observer) naturally decouple components, making unit testing easier.

3. How to Use This Guide

For each pattern you will find:

  • Intent — What problem does it solve?
  • Analogy — A simple real-world metaphor.
  • When to use — Concrete scenarios.
  • Code example — Pseudocode / JavaScript / TypeScript (adapt to your language).
  • Caveats — Common pitfalls or trade-offs.

Read the intent first, consider whether the problem matches yours, then study the code. Never apply a pattern just because it exists — apply it because it solves a real problem in your code.

4. Creational Patterns

Creational patterns deal with object creation mechanisms — abstracting instantiation to make systems more flexible and decoupled.

4.1 Singleton

Intent: Ensure a class has exactly one instance and provide a global access point.

Analogy: A country has one president at a time — anyone requesting "the president" gets the same person.

When to use: Configuration managers, connection pools, loggers — anywhere a single coordinated instance is truly required.

// Singleton (TypeScript)
class Config {
  private static instance: Config;
  private data: Record<string, string> = {};

  private constructor() {
    this.data = { env: process.env.NODE_ENV || 'production' };
  }

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

  get(key: string): string | undefined { return this.data[key]; }
}

// Usage
const config = Config.getInstance();
console.log(config.get('env')); // 'production'

Caveats: Singletons hide dependencies, complicate unit testing and can create global mutable state. Prefer dependency injection when testability matters. In JavaScript/Python, a module-level constant is often a simpler alternative.

4.2 Factory Method

Intent: Define an interface for creating objects but let subclasses decide which class to instantiate.

Analogy: A hiring agency (factory) hires different specialists depending on the client's needs — the client doesn't pick the specialist directly.

When to use: When a class can't anticipate the concrete type to create, or when you want to centralize creation logic.

// Factory Method (TypeScript)
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string) { console.log(`[CONSOLE] ${message}`); }
}

class FileLogger implements Logger {
  log(message: string) { fs.appendFileSync('app.log', `${message}\n`); }
}

// Factory method — subclasses override to provide different loggers
class App {
  protected createLogger(): Logger { return new ConsoleLogger(); }

  run() {
    const logger = this.createLogger();
    logger.log('App started');
  }
}

class ProductionApp extends App {
  protected createLogger(): Logger { return new FileLogger(); }
}

4.3 Abstract Factory

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

Analogy: A furniture catalog offers "Modern" and "Victorian" collections — each collection includes a matching chair, table and sofa.

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

class MaterialFactory implements UIFactory {
  createButton() { return new MaterialButton(); }
  createInput() { return new MaterialInput(); }
}

class BootstrapFactory implements UIFactory {
  createButton() { return new BootstrapButton(); }
  createInput() { return new BootstrapInput(); }
}

// Client code works with any factory
function renderForm(factory: UIFactory) {
  const btn = factory.createButton();
  const input = factory.createInput();
  btn.render(); input.render();
}

4.4 Builder

Intent: Construct complex objects step-by-step, allowing different representations from the same construction process.

Analogy: Ordering a custom pizza — you choose crust, sauce, cheese and toppings step by step.

// Builder (fluent API)
class QueryBuilder {
  private table = '';
  private conditions: string[] = [];
  private limitVal?: number;

  from(table: string) { this.table = table; return this; }
  where(condition: string) { this.conditions.push(condition); return this; }
  limit(n: number) { this.limitVal = n; return this; }

  build(): string {
    let sql = `SELECT * FROM ${this.table}`;
    if (this.conditions.length) sql += ` WHERE ${this.conditions.join(' AND ')}`;
    if (this.limitVal) sql += ` LIMIT ${this.limitVal}`;
    return sql;
  }
}

const query = new QueryBuilder()
  .from('users')
  .where('active = true')
  .where('age > 18')
  .limit(10)
  .build();
// SELECT * FROM users WHERE active = true AND age > 18 LIMIT 10

4.5 Prototype

Intent: Create new objects by cloning a prototypical instance, avoiding costly initialization.

Analogy: Photocopying a document — faster than writing it from scratch each time.

// Prototype (deep clone)
const defaultConfig = {
  theme: 'dark',
  locale: 'en-US',
  features: { analytics: true, debug: false }
};

function clone<T>(obj: T): T {
  return structuredClone(obj); // modern JS — or JSON.parse(JSON.stringify(obj))
}

const userConfig = clone(defaultConfig);
userConfig.features.debug = true; // doesn't affect defaultConfig

4.6 Object Pool

Intent: Reuse a pool of pre-initialized objects instead of creating/destroying them repeatedly.

Analogy: A car rental agency — cars are shared, returned after use and rented again.

// Object Pool
class Pool<T> {
  private free: T[] = [];
  private active = 0;

  constructor(private create: () => T, private max: number = 10) {}

  acquire(): T {
    if (this.free.length) return this.free.pop()!;
    if (this.active < this.max) { this.active++; return this.create(); }
    throw new Error('Pool exhausted');
  }

  release(obj: T) { this.free.push(obj); }
}

// Usage: database connection pool
const dbPool = new Pool(() => createDbConnection(), 20);
const conn = dbPool.acquire();
try { /* use connection */ } finally { dbPool.release(conn); }

5. Structural Patterns

Structural patterns deal with object composition — how classes and objects are combined to form larger structures.

5.1 Adapter

Intent: Convert one interface into another that clients expect. Makes incompatible classes work together.

Analogy: A power plug adapter that lets a European device work in a US socket.

// Adapter — wrapping an old API to match a new interface
interface ModernPayment {
  charge(amount: number, currency: string): Promise<Receipt>;
}

class LegacyPaymentGateway {
  processPayment(cents: number): { success: boolean; ref: string } {
    return { success: true, ref: 'REF-123' };
  }
}

class PaymentAdapter implements ModernPayment {
  constructor(private legacy: LegacyPaymentGateway) {}

  async charge(amount: number, currency: string): Promise<Receipt> {
    const result = this.legacy.processPayment(Math.round(amount * 100));
    if (!result.success) throw new Error('Payment failed');
    return { reference: result.ref, amount, currency };
  }
}

5.2 Bridge

Intent: Decouple an abstraction from its implementation so both can vary independently.

Analogy: A TV remote (abstraction) works with different TV brands (implementations) — you can change either without affecting the other.

// Bridge — separate shape rendering from shape logic
interface Renderer { renderCircle(x: number, y: number, r: number): void; }

class SVGRenderer implements Renderer {
  renderCircle(x: number, y: number, r: number) {
    console.log(`<circle cx="${x}" cy="${y}" r="${r}"/>`);
  }
}
class CanvasRenderer implements Renderer {
  renderCircle(x: number, y: number, r: number) {
    console.log(`ctx.arc(${x}, ${y}, ${r}, 0, Math.PI * 2)`);
  }
}

class Circle {
  constructor(private renderer: Renderer, public x: number, public y: number, public r: number) {}
  draw() { this.renderer.renderCircle(this.x, this.y, this.r); }
}

// Either renderer works
new Circle(new SVGRenderer(), 10, 10, 5).draw();
new Circle(new CanvasRenderer(), 10, 10, 5).draw();

5.3 Composite

Intent: Compose objects into tree structures; treat individual objects and compositions uniformly.

Analogy: A file system — folders contain files or other folders, and you "get size" on any item the same way.

// Composite — file system example
interface FileSystemNode {
  name: string;
  getSize(): number;
}

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

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

const root = new Directory('src');
root.add(new File('index.ts', 1200));
const utils = new Directory('utils');
utils.add(new File('helpers.ts', 800));
root.add(utils);
console.log(root.getSize()); // 2000

5.4 Decorator

Intent: Add responsibilities to objects dynamically without changing their interface.

Analogy: Adding a phone case with a kickstand — the phone's interface stays the same, but now it has extra features.

// Decorator — adding logging and timing to a service
interface DataService {
  fetch(id: string): Promise<Data>;
}

class ApiService implements DataService {
  async fetch(id: string) { return callApi(`/data/${id}`); }
}

class LoggingDecorator implements DataService {
  constructor(private inner: DataService) {}
  async fetch(id: string) {
    console.log(`Fetching ${id}`);
    const result = await this.inner.fetch(id);
    console.log(`Fetched ${id} — ${JSON.stringify(result).length} bytes`);
    return result;
  }
}

class CachingDecorator implements DataService {
  private cache = new Map<string, Data>();
  constructor(private inner: DataService) {}
  async fetch(id: string) {
    if (this.cache.has(id)) return this.cache.get(id)!;
    const result = await this.inner.fetch(id);
    this.cache.set(id, result);
    return result;
  }
}

// Stack decorators — order matters
const service = new CachingDecorator(new LoggingDecorator(new ApiService()));

5.5 Facade

Intent: Provide a simplified interface to a complex subsystem.

Analogy: A hotel concierge handles restaurant reservations, taxi bookings and event tickets — you just ask the concierge.

// Facade — simplify a complex video conversion pipeline
class VideoConverter {
  convert(inputFile: string, outputFormat: string): string {
    const video = VideoReader.read(inputFile);       // complex subsystem 1
    const codec = CodecFactory.get(outputFormat);     // complex subsystem 2
    const compressed = Compressor.compress(video, codec); // subsystem 3
    const output = AudioMixer.fix(compressed);        // subsystem 4
    return FileWriter.write(output, outputFormat);    // subsystem 5
  }
}

// Client uses one simple method
const converter = new VideoConverter();
converter.convert('movie.avi', 'mp4');

5.6 Flyweight

Intent: Share common state between many objects to reduce memory usage.

Analogy: A font rendering engine stores each character glyph once and references it everywhere, instead of duplicating it per character on the page.

// Flyweight — shared tree types in a forest simulator
class TreeType { // intrinsic state (shared)
  constructor(public name: string, public color: string, public texture: string) {}
  draw(x: number, y: number) { /* render at position */ }
}

class TreeFactory {
  private static types = new Map<string, TreeType>();

  static getType(name: string, color: string, texture: string): TreeType {
    const key = `${name}-${color}-${texture}`;
    if (!this.types.has(key)) this.types.set(key, new TreeType(name, color, texture));
    return this.types.get(key)!;
  }
}

class Tree { // extrinsic state (unique per tree)
  constructor(public x: number, public y: number, public type: TreeType) {}
  draw() { this.type.draw(this.x, this.y); }
}

// 1 million trees, but only ~10 unique TreeType objects in memory

5.7 Proxy

Intent: Provide a surrogate or placeholder to control access to another object.

Analogy: A security guard at a building entrance — checks credentials before allowing entry.

// Proxy variants

// 1. Caching Proxy
class CachingApiProxy {
  private cache = new Map();
  constructor(private api: RealApi) {}
  async get(url: string) {
    if (this.cache.has(url)) return this.cache.get(url);
    const result = await this.api.get(url);
    this.cache.set(url, result);
    return result;
  }
}

// 2. Access Control Proxy
class SecureServiceProxy {
  constructor(private service: Service, private user: User) {}
  delete(id: string) {
    if (this.user.role !== 'admin') throw new Error('Access denied');
    return this.service.delete(id);
  }
}

// 3. Virtual Proxy (lazy initialization)
class LazyImage {
  private realImage?: HeavyImage;
  constructor(private path: string) {}
  display() {
    if (!this.realImage) this.realImage = new HeavyImage(this.path);
    this.realImage.display();
  }
}

6. Behavioral Patterns

Behavioral patterns deal with communication between objects — how they interact and distribute responsibility.

6.1 Chain of Responsibility

Intent: Pass a request along a chain of handlers until one processes it.

Analogy: Customer support escalation — first-line support tries, then escalates to a manager, then to a specialist.

// Chain of Responsibility — middleware pipeline
abstract class Handler {
  protected next?: Handler;
  setNext(handler: Handler): Handler { this.next = handler; return handler; }
  handle(request: Request): Response | null {
    if (this.next) return this.next.handle(request);
    return null;
  }
}

class AuthHandler extends Handler {
  handle(req: Request) {
    if (!req.headers.authorization) return { status: 401, body: 'Unauthorized' };
    return super.handle(req);
  }
}

class RateLimitHandler extends Handler {
  handle(req: Request) {
    if (isRateLimited(req.ip)) return { status: 429, body: 'Too many requests' };
    return super.handle(req);
  }
}

class BusinessHandler extends Handler {
  handle(req: Request) { return { status: 200, body: processRequest(req) }; }
}

// Wire the chain
const chain = new AuthHandler();
chain.setNext(new RateLimitHandler()).setNext(new BusinessHandler());
const response = chain.handle(incomingRequest);

6.2 Command

Intent: Encapsulate a request as an object, enabling parameterization, queuing, undo and logging.

Analogy: A restaurant order slip — the waiter writes down the order (command) and passes it to the kitchen (receiver). The order can be modified, cancelled or replayed.

// Command — with undo support
interface Command {
  execute(): void;
  undo(): void;
}

class AddTextCommand implements Command {
  constructor(private editor: TextEditor, private text: string) {}
  execute() { this.editor.insert(this.text); }
  undo() { this.editor.deleteLastN(this.text.length); }
}

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

const history = new CommandHistory();
history.execute(new AddTextCommand(editor, 'Hello'));
history.execute(new AddTextCommand(editor, ' World'));
history.undo(); // removes ' World'

6.3 Iterator

Intent: Provide sequential access to elements of a collection without exposing its internal structure.

// Iterator — custom iterable range
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: undefined, done: true };
      }
    };
  }
}

for (const n of new Range(1, 5)) console.log(n); // 1, 2, 3, 4, 5

6.4 Mediator

Intent: Define an object that encapsulates how a set of objects interact, reducing direct dependencies.

Analogy: An air traffic control tower — planes don't communicate with each other directly; they all talk through the tower.

// Mediator — chat room
class ChatRoom {
  private users = new Map<string, User>();

  register(user: User) { this.users.set(user.name, user); user.room = this; }

  send(message: string, from: string, to: string) {
    const recipient = this.users.get(to);
    if (recipient) recipient.receive(message, from);
  }

  broadcast(message: string, from: string) {
    this.users.forEach((user, name) => {
      if (name !== from) user.receive(message, from);
    });
  }
}

6.5 Memento

Intent: Capture and externalize an object's state so it can be restored later without violating encapsulation.

Analogy: Saving a video game — you can return to any saved checkpoint.

// Memento — editor snapshots
class EditorMemento {
  constructor(public readonly content: string, public readonly cursorPos: number) {}
}

class Editor {
  private content = '';
  private cursorPos = 0;

  type(text: string) { this.content += text; this.cursorPos += text.length; }
  save(): EditorMemento { return new EditorMemento(this.content, this.cursorPos); }
  restore(m: EditorMemento) { this.content = m.content; this.cursorPos = m.cursorPos; }
}

const editor = new Editor();
editor.type('Hello');
const snap = editor.save();
editor.type(' World that I want to undo');
editor.restore(snap); // back to 'Hello'

6.6 Observer

Intent: Define a one-to-many dependency: when one object changes state, all dependents are notified automatically.

Analogy: Subscribing to a newsletter — when the author publishes, every subscriber receives the update.

// Observer — type-safe event emitter
class EventEmitter<T> {
  private listeners = new Set<(data: T) => void>();

  on(listener: (data: T) => void) { this.listeners.add(listener); return () => this.off(listener); }
  off(listener: (data: T) => void) { this.listeners.delete(listener); }
  emit(data: T) { this.listeners.forEach(fn => fn(data)); }
}

// Usage
const priceUpdates = new EventEmitter<{ symbol: string; price: number }>();
const unsub = priceUpdates.on(data => console.log(`${data.symbol}: $${data.price}`));
priceUpdates.emit({ symbol: 'AAPL', price: 178.50 });
unsub(); // unsubscribe

Caveats: Always unsubscribe when the observer is destroyed to prevent memory leaks. In complex systems, consider a dedicated event bus or message broker.

6.7 State

Intent: Allow an object to alter its behavior when its internal state changes — the object appears to change its class.

Analogy: A traffic light changes behavior based on its state (red, yellow, green) without being a different object.

// State — order lifecycle
interface OrderState {
  next(order: Order): void;
  cancel(order: Order): void;
  toString(): string;
}

class DraftState implements OrderState {
  next(order: Order) { order.setState(new ConfirmedState()); }
  cancel(order: Order) { order.setState(new CancelledState()); }
  toString() { return 'Draft'; }
}

class ConfirmedState implements OrderState {
  next(order: Order) { order.setState(new ShippedState()); }
  cancel(order: Order) { throw new Error('Cannot cancel a confirmed order'); }
  toString() { return 'Confirmed'; }
}

class ShippedState implements OrderState {
  next(order: Order) { order.setState(new DeliveredState()); }
  cancel(order: Order) { throw new Error('Cannot cancel a shipped order'); }
  toString() { return 'Shipped'; }
}

6.8 Strategy

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.

Analogy: Choosing a payment method at checkout — the process is the same, but the payment algorithm varies (card, PayPal, bank transfer).

// Strategy — compression algorithms
interface CompressionStrategy {
  compress(data: Buffer): Buffer;
  name: string;
}

class GzipStrategy implements CompressionStrategy {
  name = 'gzip';
  compress(data: Buffer) { return zlib.gzipSync(data); }
}

class BrotliStrategy implements CompressionStrategy {
  name = 'brotli';
  compress(data: Buffer) { return zlib.brotliCompressSync(data); }
}

class FileCompressor {
  constructor(private strategy: CompressionStrategy) {}
  setStrategy(s: CompressionStrategy) { this.strategy = s; }
  compress(file: string) {
    const data = fs.readFileSync(file);
    const compressed = this.strategy.compress(data);
    fs.writeFileSync(`${file}.${this.strategy.name}`, compressed);
  }
}

// Swap at runtime
const compressor = new FileCompressor(new GzipStrategy());
compressor.compress('data.json');
compressor.setStrategy(new BrotliStrategy());
compressor.compress('data.json');

6.9 Template Method

Intent: Define the skeleton of an algorithm in a base class; let subclasses override specific steps.

// Template Method — data processing pipeline
abstract class DataProcessor {
  // Template method — defines the algorithm skeleton
  process() {
    const raw = this.read();
    const parsed = this.parse(raw);
    const transformed = this.transform(parsed);
    this.save(transformed);
  }

  abstract read(): string;
  abstract parse(raw: string): Record<string, unknown>[];
  protected transform(data: Record<string, unknown>[]) { return data; } // optional override
  abstract save(data: Record<string, unknown>[]): void;
}

class CSVProcessor extends DataProcessor {
  read() { return fs.readFileSync('data.csv', 'utf-8'); }
  parse(raw: string) { return parseCSV(raw); }
  save(data: Record<string, unknown>[]) { db.insertMany(data); }
}

class JSONProcessor extends DataProcessor {
  read() { return fs.readFileSync('data.json', 'utf-8'); }
  parse(raw: string) { return JSON.parse(raw); }
  save(data: Record<string, unknown>[]) { db.insertMany(data); }
}

6.10 Visitor

Intent: Add new operations to object structures without modifying the classes.

// Visitor — AST node processing
interface ASTVisitor {
  visitNumber(node: NumberNode): number;
  visitBinary(node: BinaryNode): number;
}

class NumberNode { constructor(public value: number) {} accept(v: ASTVisitor) { return v.visitNumber(this); } }
class BinaryNode {
  constructor(public op: string, public left: ASTNode, public right: ASTNode) {}
  accept(v: ASTVisitor) { return v.visitBinary(this); }
}

class Evaluator implements ASTVisitor {
  visitNumber(node: NumberNode) { return node.value; }
  visitBinary(node: BinaryNode) {
    const l = node.left.accept(this), r = node.right.accept(this);
    if (node.op === '+') return l + r;
    if (node.op === '*') return l * r;
    throw new Error(`Unknown operator: ${node.op}`);
  }
}

// 3 + (4 * 2) = 11
const tree = new BinaryNode('+', new NumberNode(3), new BinaryNode('*', new NumberNode(4), new NumberNode(2)));
console.log(tree.accept(new Evaluator())); // 11

6.11 Null Object

Intent: Provide a "do-nothing" object to avoid null checks and simplify client code.

// Null Object
interface Logger { log(msg: string): void; }
class ConsoleLogger implements Logger { log(msg: string) { console.log(msg); } }
class NullLogger implements Logger { log() {} } // does nothing — no null checks needed

function createService(logger?: Logger) {
  const log = logger ?? new NullLogger(); // safe default
  log.log('Service created');
  return { log };
}

7. Architectural Patterns

7.1 MVC / MVP / MVVM

Intent: Separate data (Model), user interface (View), and coordination logic (Controller / Presenter / ViewModel).

  • MVC — Controller processes input, updates Model; View reads Model. Classic server-side web.
  • MVP — Presenter mediates between View and Model; View is passive. Facilitates unit testing of presentation logic.
  • MVVM — ViewModel exposes observable state; View binds to it declaratively. Common in React, Angular, Vue, SwiftUI.

7.2 Event Sourcing

Intent: Persist state as a sequence of events rather than overwriting current state. Enables full audit trails, temporal queries and replay.

7.3 CQRS

Intent: Separate read and write models to optimize each independently. Pairs well with Event Sourcing.

7.4 Repository Pattern

Intent: Abstract data access behind a collection-like interface. The application works with domain objects; the repository handles persistence details.

8. Combining Patterns

8.1 Plugin System

A plugin host combines Observer (event dispatch), Strategy (plugin-provided behavior), and Adapter (compatibility wrappers):

// Plugin system combining Observer + Strategy + Adapter
class PluginHost {
  private events = new EventEmitter<any>();
  private strategies = new Map<string, Function>();

  register(plugin: Plugin) {
    plugin.handlers.forEach(([event, handler]) => this.events.on(event, handler));
    plugin.strategies.forEach(([name, fn]) => this.strategies.set(name, fn));
  }

  emit(event: string, data: any) { this.events.emit(data); }
  getStrategy(name: string) { return this.strategies.get(name); }
}

8.2 Resource Pool

Combines Factory (creation), Object Pool (reuse), and Singleton (single pool manager) to manage expensive resources like database connections.

8.3 Middleware Pipeline

Combines Chain of Responsibility (sequential handling) with Decorator (wrapping behavior) — exactly how Express, Koa and ASP.NET middleware work.

9. Real-World Implementations

JavaScript — Observer (EventEmitter)

// Node.js built-in Observer implementation
const EventEmitter = require('events');
class OrderService extends EventEmitter {
  create(data) {
    const order = { id: generateId(), ...data };
    this.emit('orderCreated', order);
    return order;
  }
}
const svc = new OrderService();
svc.on('orderCreated', order => sendConfirmationEmail(order));
svc.on('orderCreated', order => updateInventory(order));

Python — Strategy with callables

# Python Strategy — functions as strategies
from typing import Callable, List

def quicksort(items: List[int]) -> List[int]:
    return sorted(items)

def reverse_sort(items: List[int]) -> List[int]:
    return sorted(items, reverse=True)

def process(items: List[int], strategy: Callable) -> List[int]:
    return strategy(items)

process([3, 1, 2], quicksort)      # [1, 2, 3]
process([3, 1, 2], reverse_sort)   # [3, 2, 1]

Java — Factory Method

// Java Factory Method
public interface Notification { void send(String message); }
public class EmailNotification implements Notification { public void send(String msg) { /* ... */ } }
public class SMSNotification implements Notification { public void send(String msg) { /* ... */ } }

public class NotificationFactory {
  public static Notification create(String type) {
    return switch (type) {
      case "email" -> new EmailNotification();
      case "sms" -> new SMSNotification();
      default -> throw new IllegalArgumentException("Unknown type: " + type);
    };
  }
}

10. Anti-Patterns & Common Mistakes

  • Pattern Overuse — Applying patterns where a simple function or if statement suffices. Follow YAGNI (You Aren't Gonna Need It).
  • Premature Abstraction — Creating Factory/Strategy/Observer infrastructure before you have two concrete cases.
  • God Object — A class that knows too much and does too much. Decompose using SRP and relevant patterns.
  • Golden Hammer — Using your favorite pattern everywhere. Each problem deserves its own analysis.
  • Spaghetti Observers — Overusing Observer/events creates invisible dependencies. If debugging requires tracing event chains across 5 files, reconsider the design.
  • Singleton Abuse — Using Singleton as a global variable. If you're doing Singleton.getInstance() everywhere, you've just hidden a dependency.

Rule of thumb: Refactor toward a pattern when you feel the pain of the problem it solves, not before.

11. Testing & Maintainability

Patterns improve testability by isolating responsibilities and enabling substitution:

  • Strategy & Factory — Swap real implementations with test doubles easily.
  • Observer — Test that the correct events are emitted without testing all subscribers.
  • Command — Test execute and undo independently.
  • Decorator — Test each decorator in isolation, then test the composed stack.
  • Null Object — Eliminates null-check logic that's hard to cover in tests.

12. Performance Considerations

  • Proxy / Decorator / Bridge — Add indirection (extra function calls). Negligible in most cases; measure if concerned.
  • Flyweight — Explicitly designed to reduce memory. Use when you have thousands of similar objects.
  • Object Pool — Reduces GC pressure from repeated allocation/deallocation of expensive objects.
  • Observer — Broadcasting to many observers can be slow if handlers are heavy. Consider async dispatch or batching.

Always profile before optimizing. Added clarity from patterns almost always outweighs micro-performance costs.

13. Quick-Reference Cheat Sheet

  • Need one instance? → Singleton (or module export)
  • Creating objects without specifying exact class? → Factory Method / Abstract Factory
  • Building complex objects step by step? → Builder
  • Cloning expensive objects? → Prototype
  • Reusing pre-initialized objects? → Object Pool
  • Making incompatible interfaces work? → Adapter
  • Decoupling abstraction from implementation? → Bridge
  • Tree structure with uniform API? → Composite
  • Adding behavior dynamically? → Decorator
  • Simplifying complex subsystem? → Facade
  • Sharing state to save memory? → Flyweight
  • Controlling access to an object? → Proxy
  • Passing request through handlers? → Chain of Responsibility
  • Encapsulating actions as objects? → Command
  • Traversing a collection? → Iterator
  • Reducing object-to-object coupling? → Mediator
  • Saving/restoring state? → Memento
  • Subscribing to state changes? → Observer
  • Changing behavior by state? → State
  • Swapping algorithms at runtime? → Strategy
  • Defining algorithm skeleton with customizable steps? → Template Method
  • Adding operations without modifying classes? → Visitor
  • Avoiding null checks? → Null Object

14. Checklist for Applying a Pattern

  • ☐ Is there a recurring problem the pattern addresses?
  • ☐ Do you have at least two concrete cases (not a hypothetical future need)?
  • ☐ Does it reduce duplication or clarify responsibilities?
  • ☐ Can you implement it incrementally and keep tests green?
  • ☐ Is the added indirection acceptable for maintenance and performance?
  • ☐ Does the team understand the pattern and agree on its use?
  • ☐ Does a simpler alternative (function, if-statement, configuration) solve the problem adequately?

15. FAQ

Should I memorize all 23 GoF patterns?

No. Learn 5–6 foundational patterns deeply (Factory, Strategy, Observer, Decorator, Adapter, Singleton) and understand the intent of the rest. You'll recognize when you need them naturally as you gain experience.

Are design patterns language-specific?

No. Patterns are language-agnostic concepts. However, some patterns are unnecessary in certain languages — for example, Strategy in Python is often just a function parameter, and Singleton in JavaScript is often a module export.

Is "design patterns" the same as "software architecture"?

No. Design patterns solve local, code-level problems (how to create objects, how to communicate between components). Architecture solves system-level problems (how to organize modules, services, layers). They are complementary.

When should I refactor toward a pattern?

When you notice code smells: long switch statements (→ Strategy/State), duplicated creation logic (→ Factory), classes doing too much (→ Decorator/Facade), or tight coupling (→ Observer/Mediator). Refactor when the pain is real, not hypothetical.

Can patterns hurt performance?

Most patterns add minimal overhead (one extra function call). The exceptions are patterns involving heavy allocation or deep chains. Always profile — the bottleneck is almost never a design pattern; it's I/O, algorithms, or data structures.

What's the best resource to learn patterns?

Start with Refactoring.Guru (free, visual). Then read the GoF book for depth. Practice by refactoring real code, not toy examples.

16. Glossary

GoF
Gang of Four — the four authors (Gamma, Helm, Johnson, Vlissides) of the seminal "Design Patterns" book (1994).
Creational Pattern
A pattern that deals with object creation mechanisms.
Structural Pattern
A pattern that deals with object composition and relationships.
Behavioral Pattern
A pattern that deals with communication and interaction between objects.
Indirection
An extra layer of abstraction between two components, typically to decouple them.
Composition
Building complex objects by combining simpler ones, as opposed to inheritance.
Encapsulation
Hiding internal state and implementation details behind a well-defined interface.
Polymorphism
The ability to treat objects of different types through a common interface.
YAGNI
"You Aren't Gonna Need It" — principle advising against implementing features or abstractions until they are actually needed.
Code Smell
A surface-level indicator of a deeper design problem — a hint that refactoring (possibly toward a pattern) may be needed.

17. References & Further Reading

18. Conclusion

Design patterns are tools, not rules. They encode decades of collective wisdom about how to structure code that is flexible, testable and maintainable. The key is to learn the intent behind each pattern and apply it when a real problem demands it — not before.

Start with a handful of foundational patterns (Factory, Strategy, Observer, Decorator, Adapter), practice recognizing the code smells they solve, and refactor toward them incrementally. Over time, they will become second nature.

Pick one pattern from this guide — the one closest to a problem in your current project — and refactor one small module to use it. Observe the trade-offs firsthand.