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.