Toolmingo
Guides19 min read

Design Patterns Cheat Sheet: All 23 GoF Patterns with Code Examples

Complete software design patterns reference — all 23 Gang of Four patterns grouped by category, with real code in JavaScript, Python, and TypeScript, plus when to use each pattern.

Design patterns are reusable solutions to commonly occurring problems in software design. The "Gang of Four" (GoF) book catalogued 23 patterns in 1994 — they are still the foundation of every senior engineer's vocabulary. This cheat sheet covers all 23 with code, trade-offs, and guidance on when (and when not) to use each.

Quick reference

Pattern Category One-line summary
Singleton Creational One instance, global access
Factory Method Creational Subclass decides which class to instantiate
Abstract Factory Creational Families of related objects without concrete classes
Builder Creational Construct complex objects step by step
Prototype Creational Clone existing objects
Adapter Structural Make incompatible interfaces work together
Bridge Structural Decouple abstraction from implementation
Composite Structural Tree structures of uniform objects
Decorator Structural Add behaviour without changing the class
Facade Structural Simplified interface to a complex subsystem
Flyweight Structural Share fine-grained objects to save memory
Proxy Structural Placeholder that controls access to another object
Chain of Responsibility Behavioral Pass a request along a chain of handlers
Command Behavioral Encapsulate a request as an object
Iterator Behavioral Sequential access without exposing internals
Mediator Behavioral Objects communicate through a mediator
Memento Behavioral Save and restore object state
Observer Behavioral Notify dependents of state changes
State Behavioral Alter behaviour when internal state changes
Strategy Behavioral Swap algorithms at runtime
Template Method Behavioral Skeleton algorithm, steps overridden by subclasses
Visitor Behavioral Add operations without modifying classes
Interpreter Behavioral Interpret sentences in a grammar

Creational patterns

Singleton

Ensures a class has only one instance and provides a global access point.

// TypeScript — thread-safe lazy singleton
class DatabaseConnection {
  private static instance: DatabaseConnection | null = null;
  private readonly url: string;

  private constructor(url: string) {
    this.url = url;
  }

  static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(process.env.DB_URL!);
    }
    return DatabaseConnection.instance;
  }

  query(sql: string) { /* ... */ }
}

// Usage
const db = DatabaseConnection.getInstance();
# Python — module-level singleton (simplest approach)
class _Config:
    def __init__(self):
        self.debug = False
        self.db_url = ""

config = _Config()  # import this object, not the class

# Alternative: metaclass singleton
class SingletonMeta(type):
    _instances: dict = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class AppConfig(metaclass=SingletonMeta):
    pass

Use when: logging, config, connection pools, caches.
Avoid when: unit tests become hard (global state is hard to reset), or you need multiple environments.


Factory Method

Defines an interface for creating an object but lets subclasses decide which class to instantiate.

// TypeScript
interface Notification {
  send(message: string): void;
}

class EmailNotification implements Notification {
  send(message: string) { console.log(`Email: ${message}`); }
}

class SMSNotification implements Notification {
  send(message: string) { console.log(`SMS: ${message}`); }
}

abstract class NotificationService {
  abstract createNotification(): Notification; // factory method

  notify(message: string) {
    const notification = this.createNotification();
    notification.send(message);
  }
}

class EmailService extends NotificationService {
  createNotification(): Notification { return new EmailNotification(); }
}

class SMSService extends NotificationService {
  createNotification(): Notification { return new SMSNotification(); }
}

Use when: you don't know the exact class to create until runtime, or subclasses should control object creation.


Abstract Factory

Creates families of related objects without specifying concrete classes.

# Python
from abc import ABC, abstractmethod

class Button(ABC):
    @abstractmethod
    def render(self): pass

class Checkbox(ABC):
    @abstractmethod
    def render(self): pass

# Windows family
class WindowsButton(Button):
    def render(self): return "<WinButton>"

class WindowsCheckbox(Checkbox):
    def render(self): return "<WinCheckbox>"

# Mac family
class MacButton(Button):
    def render(self): return "<MacButton>"

class MacCheckbox(Checkbox):
    def render(self): return "<MacCheckbox>"

# Abstract factory
class UIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button: pass
    @abstractmethod
    def create_checkbox(self) -> Checkbox: pass

class WindowsFactory(UIFactory):
    def create_button(self): return WindowsButton()
    def create_checkbox(self): return WindowsCheckbox()

class MacFactory(UIFactory):
    def create_button(self): return MacButton()
    def create_checkbox(self): return MacCheckbox()

# Client
def build_ui(factory: UIFactory):
    btn = factory.create_button()
    chk = factory.create_checkbox()
    print(btn.render(), chk.render())

build_ui(WindowsFactory())  # <WinButton> <WinCheckbox>

Use when: your code needs to work with families of related objects (UI themes, DB drivers, cloud providers).


Builder

Constructs complex objects step by step, separating construction from representation.

// TypeScript
class QueryBuilder {
  private table = "";
  private conditions: string[] = [];
  private columns = "*";
  private limitVal: number | null = null;

  from(table: string): this {
    this.table = table;
    return this;
  }

  select(...cols: string[]): this {
    this.columns = cols.join(", ");
    return this;
  }

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  limit(n: number): this {
    this.limitVal = n;
    return this;
  }

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

const sql = new QueryBuilder()
  .from("users")
  .select("id", "email")
  .where("active = true")
  .limit(10)
  .build();
// SELECT id, email FROM users WHERE active = true LIMIT 10

Use when: constructing objects with many optional parameters (avoids "telescoping constructor" problem).


Prototype

Creates new objects by cloning an existing object.

// JavaScript — built-in structuredClone
const original = {
  name: "Template",
  config: { retries: 3, timeout: 5000 },
  tags: ["default"],
};

const clone = structuredClone(original);
clone.name = "My Clone";
clone.tags.push("custom"); // doesn't affect original

// Class-based prototype
class Shape {
  constructor(public color: string, public x: number, public y: number) {}

  clone(): this {
    return structuredClone(this) as this;
  }
}

const circle = new Shape("red", 10, 20);
const circle2 = circle.clone();
circle2.x = 50;

Use when: object creation is expensive (DB lookup, complex init) and you can start from a known-good state.


Structural patterns

Adapter

Makes an incompatible interface compatible with what the client expects.

// TypeScript — class adapter
interface JsonLogger {
  logJson(data: object): void;
}

// Third-party library with different interface
class LegacyLogger {
  writeLog(message: string, level: string): void {
    console.log(`[${level}] ${message}`);
  }
}

class LoggerAdapter implements JsonLogger {
  constructor(private legacy: LegacyLogger) {}

  logJson(data: object): void {
    const { level = "INFO", ...rest } = data as any;
    this.legacy.writeLog(JSON.stringify(rest), level);
  }
}

const adapter = new LoggerAdapter(new LegacyLogger());
adapter.logJson({ level: "WARN", message: "disk full", disk: "/var" });

Use when: integrating third-party code or legacy systems with a different interface.


Bridge

Decouples an abstraction from its implementation so both can vary independently.

# Python
from abc import ABC, abstractmethod

class Renderer(ABC):
    @abstractmethod
    def render_circle(self, radius: float): pass

class SVGRenderer(Renderer):
    def render_circle(self, r): return f'<circle r="{r}"/>'

class CanvasRenderer(Renderer):
    def render_circle(self, r): return f"ctx.arc(0,0,{r},0,2*Math.PI)"

class Shape(ABC):
    def __init__(self, renderer: Renderer):
        self.renderer = renderer  # bridge to implementation

    @abstractmethod
    def draw(self): pass

class Circle(Shape):
    def __init__(self, renderer: Renderer, radius: float):
        super().__init__(renderer)
        self.radius = radius

    def draw(self):
        return self.renderer.render_circle(self.radius)

svg_circle = Circle(SVGRenderer(), 10)
canvas_circle = Circle(CanvasRenderer(), 10)

Use when: you want to avoid a class explosion from combining multiple dimensions (shape × renderer, remote × platform).


Composite

Composes objects into tree structures. Clients treat individual objects and compositions uniformly.

// TypeScript — file system tree
interface FileSystemItem {
  name: string;
  size(): number;
  print(indent?: string): void;
}

class File implements FileSystemItem {
  constructor(public name: string, private bytes: number) {}
  size() { return this.bytes; }
  print(indent = "") { console.log(`${indent}📄 ${this.name} (${this.bytes}B)`); }
}

class Directory implements FileSystemItem {
  private children: FileSystemItem[] = [];
  constructor(public name: string) {}

  add(item: FileSystemItem) { this.children.push(item); }
  size() { return this.children.reduce((s, c) => s + c.size(), 0); }
  print(indent = "") {
    console.log(`${indent}📁 ${this.name}/`);
    this.children.forEach(c => c.print(indent + "  "));
  }
}

const root = new Directory("root");
const src = new Directory("src");
src.add(new File("index.ts", 1200));
src.add(new File("utils.ts", 800));
root.add(src);
root.add(new File("README.md", 450));
root.print();
// 📁 root/
//   📁 src/
//     📄 index.ts (1200B)
//     📄 utils.ts (800B)
//   📄 README.md (450B)

Use when: working with tree structures (file systems, UI components, org charts, menus).


Decorator

Attaches additional responsibilities to an object dynamically without altering the class.

# Python — HTTP middleware example
from functools import wraps
import time

def timing(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.3f}s")
        return result
    return wrapper

def retry(times=3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == times - 1:
                        raise
                    print(f"Retry {attempt + 1}/{times}: {e}")
        return wrapper
    return decorator

@timing
@retry(times=3)
def fetch_data(url: str):
    import urllib.request
    with urllib.request.urlopen(url) as r:
        return r.read()
// TypeScript — class decorator (ES2023 decorators)
function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: unknown[]) {
    console.log(`${key}(${JSON.stringify(args)})`);
    const result = original.apply(this, args);
    console.log(`${key} →`, result);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a: number, b: number) { return a + b; }
}

Use when: you need to add logging, caching, auth, timing, or validation without modifying the original class.


Facade

Provides a simplified interface to a complex subsystem.

// TypeScript — video conversion facade
class VideoDecoder { decode(file: string) { return `decoded:${file}`; } }
class AudioMixer { mix(audio: string) { return `mixed:${audio}`; } }
class VideoEncoder { encode(video: string, format: string) { return `${video}.${format}`; } }
class FileWriter { write(data: string, dest: string) { console.log(`Written: ${dest}`); } }

// Facade hides all the complexity
class VideoConverterFacade {
  private decoder = new VideoDecoder();
  private mixer = new AudioMixer();
  private encoder = new VideoEncoder();
  private writer = new FileWriter();

  convert(input: string, outputFormat: string): void {
    const video = this.decoder.decode(input);
    const audio = this.mixer.mix(video);
    const encoded = this.encoder.encode(audio, outputFormat);
    this.writer.write(encoded, `output.${outputFormat}`);
  }
}

// Client only sees one method
new VideoConverterFacade().convert("movie.avi", "mp4");

Use when: a complex subsystem has many moving parts and you want to expose a simple API.


Flyweight

Uses sharing to efficiently support a large number of fine-grained objects.

# Python — character rendering (classic GoF example)
class CharacterGlyph:
    """Intrinsic state — shared, immutable."""
    def __init__(self, char: str, font: str, size: int):
        self.char = char
        self.font = font
        self.size = size

    def render(self, x: int, y: int):
        print(f"Render '{self.char}' ({self.font} {self.size}pt) at {x},{y}")

class GlyphFactory:
    _glyphs: dict[tuple, CharacterGlyph] = {}

    @classmethod
    def get(cls, char: str, font: str, size: int) -> CharacterGlyph:
        key = (char, font, size)
        if key not in cls._glyphs:
            cls._glyphs[key] = CharacterGlyph(char, font, size)
            print(f"  [cache miss] created glyph for {key}")
        return cls._glyphs[key]

# 10,000 'A' characters share ONE object
for i in range(10_000):
    glyph = GlyphFactory.get("A", "Arial", 12)
    glyph.render(i * 10, 0)  # x, y are extrinsic state

Use when: you have a huge number of similar objects that differ only in a small amount of extrinsic state (game particles, text rendering, map icons).


Proxy

Provides a surrogate or placeholder for another object to control access.

// TypeScript — lazy loading proxy
interface Image {
  display(): void;
}

class RealImage implements Image {
  private filename: string;

  constructor(filename: string) {
    this.filename = filename;
    this.loadFromDisk(); // expensive operation
  }

  private loadFromDisk() {
    console.log(`Loading ${this.filename} from disk...`);
  }

  display() {
    console.log(`Displaying ${this.filename}`);
  }
}

class ImageProxy implements Image {
  private realImage: RealImage | null = null;
  constructor(private filename: string) {} // no loading yet

  display() {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename); // lazy load
    }
    this.realImage.display();
  }
}

// Client code
const img = new ImageProxy("photo.jpg");
// RealImage not loaded yet
img.display(); // loads now
img.display(); // uses cached RealImage

Proxy types:

Type Purpose
Virtual proxy Lazy loading expensive objects
Protection proxy Access control / auth checks
Remote proxy Local stub for a remote object (RPC)
Caching proxy Cache results of expensive operations
Logging proxy Log all method calls

Behavioral patterns

Chain of Responsibility

Passes a request along a chain of handlers; each decides to handle or forward.

# Python — HTTP middleware chain
from abc import ABC, abstractmethod
from typing import Optional

class Handler(ABC):
    def __init__(self):
        self._next: Optional["Handler"] = None

    def set_next(self, handler: "Handler") -> "Handler":
        self._next = handler
        return handler  # enables chaining

    def handle(self, request: dict) -> Optional[str]:
        if self._next:
            return self._next.handle(request)
        return None

class AuthHandler(Handler):
    def handle(self, request):
        if not request.get("token"):
            return "401 Unauthorized"
        return super().handle(request)

class RateLimitHandler(Handler):
    def handle(self, request):
        if request.get("rate_limit_exceeded"):
            return "429 Too Many Requests"
        return super().handle(request)

class RouteHandler(Handler):
    def handle(self, request):
        return f"200 OK: {request.get('path', '/')}"

# Build chain
auth = AuthHandler()
rate = RateLimitHandler()
route = RouteHandler()
auth.set_next(rate).set_next(route)

print(auth.handle({"token": "abc", "path": "/api"}))     # 200 OK
print(auth.handle({}))                                     # 401 Unauthorized

Command

Encapsulates a request as an object, enabling undo/redo, queuing, and logging.

// TypeScript — text editor with undo
interface Command {
  execute(): void;
  undo(): void;
}

class TextEditor {
  private text = "";
  getText() { return this.text; }
  insertText(pos: number, text: string) {
    this.text = this.text.slice(0, pos) + text + this.text.slice(pos);
  }
  deleteText(pos: number, length: number) {
    this.text = this.text.slice(0, pos) + this.text.slice(pos + length);
  }
}

class InsertCommand implements Command {
  constructor(
    private editor: TextEditor,
    private pos: number,
    private text: string,
  ) {}
  execute() { this.editor.insertText(this.pos, this.text); }
  undo() { this.editor.deleteText(this.pos, this.text.length); }
}

class CommandHistory {
  private history: Command[] = [];

  execute(cmd: Command) {
    cmd.execute();
    this.history.push(cmd);
  }

  undo() {
    this.history.pop()?.undo();
  }
}

const editor = new TextEditor();
const history = new CommandHistory();

history.execute(new InsertCommand(editor, 0, "Hello"));
history.execute(new InsertCommand(editor, 5, " World"));
console.log(editor.getText()); // Hello World
history.undo();
console.log(editor.getText()); // Hello

Iterator

Provides sequential access to elements of a collection without exposing its internals.

# Python — custom range iterator
class Range:
    def __init__(self, start: int, stop: int, step: int = 1):
        self.start = start
        self.stop = stop
        self.step = step

    def __iter__(self):
        current = self.start
        while current < self.stop:
            yield current
            current += self.step

for n in Range(0, 10, 2):
    print(n)  # 0 2 4 6 8

# Tree iterator (DFS)
class TreeNode:
    def __init__(self, val, children=None):
        self.val = val
        self.children = children or []

    def __iter__(self):
        yield self.val
        for child in self.children:
            yield from child

Modern languages build iterators into the language (for...of in JS, for in Python, range-for in C++).


Mediator

Defines an object that encapsulates how a set of objects interact, reducing direct dependencies.

// TypeScript — chat room mediator
interface ChatMediator {
  sendMessage(message: string, sender: User): void;
  addUser(user: User): void;
}

class ChatRoom implements ChatMediator {
  private users: User[] = [];

  addUser(user: User) { this.users.push(user); }

  sendMessage(message: string, sender: User) {
    this.users
      .filter(u => u !== sender)
      .forEach(u => u.receive(`${sender.name}: ${message}`));
  }
}

class User {
  constructor(public name: string, private mediator: ChatMediator) {}

  send(message: string) { this.mediator.sendMessage(message, this); }
  receive(message: string) { console.log(`[${this.name}] ${message}`); }
}

const room = new ChatRoom();
const alice = new User("Alice", room);
const bob = new User("Bob", room);
room.addUser(alice);
room.addUser(bob);
alice.send("Hello!");  // [Bob] Alice: Hello!

Use when: you have many-to-many communication between objects (air traffic control, UI event bus, chat rooms).


Memento

Captures an object's state so it can be restored later, without violating encapsulation.

# Python — text editor history
from dataclasses import dataclass
from typing import Optional

@dataclass
class Memento:
    content: str
    cursor: int

class Editor:
    def __init__(self):
        self._content = ""
        self._cursor = 0

    def type(self, text: str):
        self._content += text
        self._cursor += len(text)

    def save(self) -> Memento:
        return Memento(self._content, self._cursor)

    def restore(self, memento: Memento):
        self._content = memento.content
        self._cursor = memento.cursor

    def __str__(self):
        return f"'{self._content}' (cursor={self._cursor})"

class History:
    def __init__(self):
        self._stack: list[Memento] = []

    def push(self, m: Memento): self._stack.append(m)
    def pop(self) -> Optional[Memento]:
        return self._stack.pop() if self._stack else None

editor = Editor()
history = History()

editor.type("Hello")
history.push(editor.save())
editor.type(" World")
print(editor)                # 'Hello World' (cursor=11)

editor.restore(history.pop())
print(editor)                # 'Hello' (cursor=5)

Observer

Defines a one-to-many dependency so that when one object changes state, all dependents are notified.

// TypeScript — event emitter / pub-sub
type EventMap = Record<string, unknown[]>;

class EventEmitter<T extends EventMap> {
  private listeners = new Map<keyof T, Set<(...args: unknown[]) => void>>();

  on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this {
    if (!this.listeners.has(event)) this.listeners.set(event, new Set());
    this.listeners.get(event)!.add(listener as any);
    return this;
  }

  off<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this {
    this.listeners.get(event)?.delete(listener as any);
    return this;
  }

  emit<K extends keyof T>(event: K, ...args: T[K]): void {
    this.listeners.get(event)?.forEach(fn => fn(...args));
  }
}

// Usage
type StoreEvents = {
  change: [key: string, value: unknown];
  reset: [];
};

const store = new EventEmitter<StoreEvents>();
store.on("change", (key, value) => console.log(`${key} = ${value}`));
store.emit("change", "user", { id: 1 });  // user = [object Object]

Observer vs Pub/Sub: Observer — subscribers know the publisher. Pub/Sub — subscribers know only the channel (fully decoupled, often via a message broker).


State

Allows an object to alter its behaviour when its internal state changes (appears to change its class).

# Python — traffic light
from abc import ABC, abstractmethod

class TrafficLight:
    def __init__(self):
        self._state: "LightState" = RedState(self)

    def set_state(self, state: "LightState"):
        self._state = state

    def change(self):
        self._state.change()

class LightState(ABC):
    def __init__(self, light: TrafficLight):
        self.light = light

    @abstractmethod
    def change(self): pass

class RedState(LightState):
    def change(self):
        print("Red → Green")
        self.light.set_state(GreenState(self.light))

class GreenState(LightState):
    def change(self):
        print("Green → Yellow")
        self.light.set_state(YellowState(self.light))

class YellowState(LightState):
    def change(self):
        print("Yellow → Red")
        self.light.set_state(RedState(self.light))

light = TrafficLight()
for _ in range(4):
    light.change()
# Red → Green, Green → Yellow, Yellow → Red, Red → Green

Use when: an object's behaviour depends heavily on its state and state transitions are complex. Replaces large if/switch blocks.


Strategy

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

// TypeScript — sorting strategy
interface SortStrategy<T> {
  sort(data: T[]): T[];
}

class BubbleSort<T> implements SortStrategy<T> {
  sort(data: T[]): T[] {
    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 NativeSort<T> implements SortStrategy<T> {
  sort(data: T[]): T[] { return [...data].sort(); }
}

class Sorter<T> {
  constructor(private strategy: SortStrategy<T>) {}

  setStrategy(s: SortStrategy<T>) { this.strategy = s; }
  sort(data: T[]) { return this.strategy.sort(data); }
}

const sorter = new Sorter(new NativeSort<number>());
console.log(sorter.sort([3, 1, 4, 1, 5])); // [1, 1, 3, 4, 5]
sorter.setStrategy(new BubbleSort());
console.log(sorter.sort([3, 1, 4, 1, 5]));

Strategy vs State: Strategy is set by the client and usually stays. State transitions itself.


Template Method

Defines the skeleton of an algorithm in a base class, deferring some steps to subclasses.

# Python — data export pipeline
from abc import ABC, abstractmethod

class DataExporter(ABC):
    # Template method — fixed skeleton
    def export(self, data: list) -> str:
        validated = self.validate(data)
        transformed = self.transform(validated)
        formatted = self.format(transformed)
        return self.write(formatted)

    def validate(self, data):
        return [d for d in data if d]  # default: remove nulls

    @abstractmethod
    def transform(self, data): pass

    @abstractmethod
    def format(self, data) -> str: pass

    def write(self, content: str) -> str:
        return content  # default: return as string

class CSVExporter(DataExporter):
    def transform(self, data): return data
    def format(self, data) -> str:
        return "\n".join(",".join(str(v) for v in row) for row in data)

class JSONExporter(DataExporter):
    def transform(self, data): return [{"row": i, "data": d} for i, d in enumerate(data)]
    def format(self, data) -> str:
        import json; return json.dumps(data, indent=2)

csv = CSVExporter()
print(csv.export([[1, "Alice"], [2, "Bob"]]))

Visitor

Lets you add further operations to objects without modifying them.

// TypeScript — AST node processing
interface ASTNode {
  accept(visitor: Visitor): string;
}

class NumberNode implements ASTNode {
  constructor(public value: number) {}
  accept(v: Visitor) { return v.visitNumber(this); }
}

class AddNode implements ASTNode {
  constructor(public left: ASTNode, public right: ASTNode) {}
  accept(v: Visitor) { return v.visitAdd(this); }
}

interface Visitor {
  visitNumber(node: NumberNode): string;
  visitAdd(node: AddNode): string;
}

class Evaluator implements Visitor {
  visitNumber(n: NumberNode) { return String(n.value); }
  visitAdd(n: AddNode) {
    return String(Number(n.left.accept(this)) + Number(n.right.accept(this)));
  }
}

class Printer implements Visitor {
  visitNumber(n: NumberNode) { return String(n.value); }
  visitAdd(n: AddNode) { return `(${n.left.accept(this)} + ${n.right.accept(this)})`; }
}

// AST for: (1 + 2) + 3
const ast = new AddNode(new AddNode(new NumberNode(1), new NumberNode(2)), new NumberNode(3));
console.log(ast.accept(new Printer()));    // ((1 + 2) + 3)
console.log(ast.accept(new Evaluator())); // 6

Interpreter

Defines a grammatical representation and an interpreter for a language.

# Python — simple math expression interpreter
class Expression:
    def interpret(self, context: dict) -> int: ...

class Number(Expression):
    def __init__(self, value: int): self.value = value
    def interpret(self, ctx): return self.value

class Variable(Expression):
    def __init__(self, name: str): self.name = name
    def interpret(self, ctx): return ctx.get(self.name, 0)

class Add(Expression):
    def __init__(self, left, right): self.left, self.right = left, right
    def interpret(self, ctx): return self.left.interpret(ctx) + self.right.interpret(ctx)

class Multiply(Expression):
    def __init__(self, left, right): self.left, self.right = left, right
    def interpret(self, ctx): return self.left.interpret(ctx) * self.right.interpret(ctx)

# Interpret: x * 2 + y
expr = Add(Multiply(Variable("x"), Number(2)), Variable("y"))
print(expr.interpret({"x": 5, "y": 3}))  # 13

Use when: you need to process a simple grammar — config DSLs, expression parsers, SQL-like query builders.


When to use which pattern

Situation Pattern
Need exactly one instance Singleton
Object creation logic is complex or conditional Factory Method / Abstract Factory
Optional parameters on object construction Builder
Expensive object creation, clone instead Prototype
Old API → new API Adapter
Add features without subclassing Decorator
Simplify a complex subsystem Facade
Tree of same-type objects Composite
Swap algorithms at runtime Strategy
Object behaviour changes with state State
Undo/redo, job queues Command
Notify multiple dependents of change Observer
Many objects sharing state Flyweight
Cross-cutting concerns (logging, auth, cache) Proxy
Decouple many-to-many communication Mediator
Save and restore snapshots Memento
Process a language / grammar Interpreter
Add operations without modifying classes Visitor
Fixed algorithm, variable steps Template Method
Pass request through handlers Chain of Responsibility

Pattern categories explained

Category Count Core idea
Creational 5 How objects are created
Structural 7 How objects are composed
Behavioral 11 How objects communicate

Common mistakes

Mistake Problem Fix
Overusing Singleton Global state → hard to test Use DI instead; pass dependencies explicitly
Pattern where none is needed Over-engineering Only add patterns when the problem is clearly present
Factory when a constructor is enough Extra complexity Start simple; refactor to Factory when needed
Confusing Strategy and State Wrong pattern chosen Strategy = client swaps algorithm. State = object transitions itself
Observer memory leaks Listeners not removed Always call off() / removeEventListener() in cleanup
Decorators that modify behaviour silently Surprises for callers Keep decorator side effects obvious and documented
Visitor forcing every class to implement Breaks Open/Closed Use only when you have stable class hierarchy and frequently add operations
Builder where a config object is enough Too much boilerplate Prefer a plain options object for <5 parameters

Design patterns vs SOLID principles

Principle Design patterns that enforce it
Single Responsibility Facade, Command, Mediator
Open/Closed Strategy, Decorator, Visitor
Liskov Substitution Factory Method, Template Method
Interface Segregation Adapter, Abstract Factory
Dependency Inversion Factory Method, Abstract Factory, Bridge

FAQ

Do I need to memorize all 23 patterns?
No. Know Singleton, Factory, Builder, Adapter, Decorator, Observer, Strategy, and Command — you'll use these 80% of the time. The rest you'll reach for when a specific problem clearly matches.

Are design patterns language-specific?
No, but some patterns are built into certain languages. Python has decorators as a language feature. JavaScript has iterators and generators. The pattern is the concept; the implementation varies.

What's the difference between Decorator and Proxy?
Proxy controls access to the object and often has the same interface. Decorator adds behaviour and is usually composed at creation time. In practice the lines blur — both wrap an object.

Factory Method vs Abstract Factory?
Factory Method: one factory method, subclasses decide the concrete class. Abstract Factory: multiple factory methods for a family of related objects. Use Abstract Factory when you need product families (e.g., UI components for Windows vs Mac).

When is Singleton acceptable?
Logger, config, connection pool — objects that are truly global and stateless (or where shared mutable state is intentional). Avoid it for service-layer objects; use dependency injection instead.

Are there patterns beyond GoF?
Yes: Repository, CQRS, Event Sourcing, Saga, Circuit Breaker, Strangler Fig, BFF (Backend For Frontend) are widely used in modern distributed systems. GoF covers OOP-level patterns; enterprise patterns cover system-level concerns.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools