cheat sheet

Design Patterns

A practical reference for classic OOP design patterns — Strategy, Observer, Factory, Adapter, Decorator, Repository — with examples in Python, TypeScript, and Go.

Design Patterns — Gang-of-Four Essentials and Modern Variants

What it is

Design patterns are reusable solutions to recurring problems in object-oriented design. The canonical catalog comes from the 1994 Design Patterns book by Gamma, Helm, Johnson, and Vlissides (the "Gang of Four" or GoF), which described 23 patterns split into three families: creational (how objects are made), structural (how objects compose), and behavioural (how objects collaborate). Patterns are not code to copy verbatim — they are vocabulary. When two engineers say "use a Strategy here", they share a precise mental model in three words.

In 2026 the practical subset that earns its keep in modern codebases is small: Strategy, Observer, Factory, Adapter, Decorator, Repository, Command, State, and Iterator. Patterns like Singleton and Abstract Factory survive mostly as cautionary tales — they encode global state and verbosity that modern dependency injection makes unnecessary. Functional languages dissolve many GoF patterns into language features (first-class functions absorb Strategy and Command; closures replace State).

When to reach for a pattern

A pattern is the right tool when you have at least two real instances of the problem it solves. Patterns applied speculatively almost always cost more than they save — they add indirection, names, and types that future readers must trace. The rule of thumb is "rule of three": refactor to a pattern when the third concrete case arrives, not the first.

SymptomLikely pattern
Big if/else or switch on a type tagStrategy, State, or polymorphism
Constructor takes 7+ argumentsBuilder or parameter object
One change ripples through many call sitesObserver or pub/sub
Need to wrap an old API to fit a new oneAdapter
Want to add behaviour without subclassingDecorator
Multiple data sources behind one interfaceRepository
Adding a new variant requires touching many filesFactory + interface
Step-by-step process with reusable stepsTemplate Method or pipeline
Action must be queued, undone, or loggedCommand

Strategy

The Strategy pattern encapsulates an algorithm behind an interface so the caller can swap implementations at runtime. It is the workhorse of object-oriented design — most "polymorphism" in real code is Strategy by another name. Reach for it whenever you have a switch on type, a config flag that picks between two implementations, or a unit test that needs to stub a behaviour.

typescript
// TypeScript — Strategy interface + two implementations
interface PricingStrategy {
  price(cart: Cart): number;
}

class StandardPricing implements PricingStrategy {
  price(cart: Cart): number {
    return cart.items.reduce((sum, i) => sum + i.price * i.qty, 0);
  }
}

class BlackFridayPricing implements PricingStrategy {
  price(cart: Cart): number {
    const total = cart.items.reduce((s, i) => s + i.price * i.qty, 0);
    return total * 0.7;
  }
}

class Checkout {
  constructor(private strategy: PricingStrategy) {}
  total(cart: Cart) { return this.strategy.price(cart); }
}

const checkout = new Checkout(new BlackFridayPricing());
console.log(checkout.total({ items: [{ price: 100, qty: 2 }] }));

Output:

text
140

In Python, Strategy collapses to a function-typed parameter — the interface is the function signature:

python
from typing import Callable
from dataclasses import dataclass

@dataclass
class Cart:
    items: list[tuple[float, int]]   # (price, qty)

PricingStrategy = Callable[[Cart], float]

def standard(cart: Cart) -> float:
    return sum(p * q for p, q in cart.items)

def black_friday(cart: Cart) -> float:
    return standard(cart) * 0.7

def checkout(cart: Cart, strategy: PricingStrategy) -> float:
    return strategy(cart)

print(checkout(Cart([(100, 2)]), black_friday))
bash
python checkout.py

Output:

text
140.0

If your Strategy interface has a single method, prefer a plain function (Python Callable, TS function type, Java @FunctionalInterface). Reserve classes for strategies that hold state or are configured at construction time.

Observer

The Observer pattern lets one object (the subject) notify a list of dependent objects (the observers) when its state changes. It is the foundation of every event system, reactive framework, and pub/sub bus you have ever touched. Use it when a change in one place must fan out to many listeners without the subject knowing who they are.

python
# Python — minimal Observer
from typing import Callable

class EventBus:
    def __init__(self) -> None:
        self._handlers: dict[str, list[Callable]] = {}

    def on(self, event: str, handler: Callable) -> None:
        self._handlers.setdefault(event, []).append(handler)

    def emit(self, event: str, *args, **kwargs) -> None:
        for h in self._handlers.get(event, []):
            h(*args, **kwargs)

bus = EventBus()
bus.on("user.created", lambda u: print(f"audit: created {u}"))
bus.on("user.created", lambda u: print(f"email: welcome {u}"))
bus.emit("user.created", "alice@example.com")
bash
python events.py

Output:

text
audit: created alice@example.com
email: welcome alice@example.com

Modern equivalents include Node's EventEmitter, Go channels, Rx streams, and the DOM addEventListener. The browser-side React useEffect + state hooks form a constrained Observer where React itself is the bus.

Observers leak memory. If a long-lived subject holds references to short-lived observers (a global event bus referencing a closed dialog), the dialog never garbage-collects. Always provide a .off() / unsubscribe() method and call it during teardown.

Factory

A Factory is a function or method whose job is to construct objects. The Factory Method variant returns objects through an inheritance hierarchy; the Abstract Factory variant returns families of related objects. In modern code the simple form — a function named create_x or make_x that returns the right concrete class — is by far the most common.

Use a factory when (a) construction logic is non-trivial (validation, lookups, default wiring), (b) the caller should not depend on the concrete type, or (c) you want to defer the choice of class to runtime config.

typescript
// TypeScript — Factory function returning a tagged union
type Notifier =
  | { type: "email"; send: (to: string, msg: string) => void }
  | { type: "sms"; send: (to: string, msg: string) => void }
  | { type: "noop"; send: (to: string, msg: string) => void };

function createNotifier(kind: "email" | "sms" | "noop"): Notifier {
  switch (kind) {
    case "email":
      return { type: "email", send: (to, m) => console.log(`MAIL ${to}: ${m}`) };
    case "sms":
      return { type: "sms", send: (to, m) => console.log(`SMS ${to}: ${m}`) };
    case "noop":
      return { type: "noop", send: () => {} };
  }
}

const n = createNotifier(process.env.NOTIFIER === "sms" ? "sms" : "email");
n.send("alice@example.com", "hello");
bash
NOTIFIER=sms node notifier.js

Output:

text
SMS alice@example.com: hello

"Abstract Factory" — a factory of factories — is rarely worth the ceremony in 2026. If you find yourself writing one, ask whether a dependency-injection container would solve the same problem with less code.

Adapter

An Adapter wraps an object so its interface matches the one the caller expects. It is the "USB-to-PS/2" of software. Use it when integrating a third-party library, a legacy module, or a class you cannot modify, and you do not want its quirks to leak through your domain code.

python
# Python — Adapter between a legacy logger and a modern Logger interface
class LegacyLogger:
    def write(self, level: int, text: str) -> None:
        print(f"[{level}] {text}")

class Logger:
    """Modern interface used everywhere in this codebase."""
    def info(self, msg: str) -> None: ...
    def error(self, msg: str) -> None: ...

class LegacyLoggerAdapter(Logger):
    LEVEL = {"info": 1, "error": 3}
    def __init__(self, legacy: LegacyLogger) -> None:
        self._legacy = legacy
    def info(self, msg: str) -> None:
        self._legacy.write(self.LEVEL["info"], msg)
    def error(self, msg: str) -> None:
        self._legacy.write(self.LEVEL["error"], msg)

log: Logger = LegacyLoggerAdapter(LegacyLogger())
log.info("startup")
log.error("disk full")
bash
python adapter.py

Output:

text
[1] startup
[3] disk full

The adapter is the only place in the codebase that knows about the legacy contract — every other consumer sees the modern Logger shape. If the legacy library is replaced, only the adapter changes.

Decorator

The Decorator pattern wraps an object to add behaviour without modifying its class. It is the cleanest way to layer cross-cutting concerns — logging, caching, retries, authorization — onto an existing API. Python's @decorator syntax and JavaScript's higher-order functions are decorators baked into the language.

python
# Python — caching decorator on top of a slow function
from functools import lru_cache
import time

@lru_cache(maxsize=128)
def fetch_user(user_id: int) -> dict:
    time.sleep(0.5)
    return {"id": user_id, "name": f"user-{user_id}"}

t0 = time.time(); fetch_user(1)
t1 = time.time(); fetch_user(1)   # cached, instant
t2 = time.time()
print(f"first: {t1-t0:.2f}s, second: {t2-t1:.4f}s")
bash
python cache_decorator.py

Output:

text
first: 0.50s, second: 0.0001s

For class-based decoration — the "wrap and forward" pattern — the structure is uniform:

typescript
// TypeScript — logging Decorator over a Repository interface
interface UserRepo {
  findById(id: number): Promise<User | null>;
}

class LoggingRepo implements UserRepo {
  constructor(private inner: UserRepo) {}
  async findById(id: number): Promise<User | null> {
    const t0 = Date.now();
    const result = await this.inner.findById(id);
    console.log(`findById(${id}) → ${result ? "hit" : "miss"} in ${Date.now() - t0}ms`);
    return result;
  }
}

const repo: UserRepo = new LoggingRepo(new SqlUserRepo(db));

Each layer adds one concern. new RetryingRepo(new LoggingRepo(new CachingRepo(new SqlUserRepo(db)))) reads top-to-bottom as the order requests flow through.

Decorator pairs naturally with the Repository pattern (below) and the Chain of Responsibility pattern. If you find yourself with one giant UserService doing caching + logging + retries + DB access, peel each off into its own decorator.

Repository

The Repository pattern hides persistence behind a collection-like interface. Domain code talks to UserRepo.findById(1); the repo's implementation can be Postgres, an in-memory dict for tests, or a remote HTTP API. This decoupling makes tests fast (no DB), migrations safe (swap implementations), and domain code testable in isolation.

typescript
// TypeScript — Repository interface + two implementations
interface User { id: number; email: string; name: string }

interface UserRepository {
  findById(id: number): Promise<User | null>;
  save(user: User): Promise<void>;
}

// Production implementation
class PostgresUserRepository implements UserRepository {
  constructor(private db: Pool) {}
  async findById(id: number) {
    const { rows } = await this.db.query("SELECT * FROM users WHERE id=$1", [id]);
    return rows[0] ?? null;
  }
  async save(u: User) {
    await this.db.query(
      "INSERT INTO users(id,email,name) VALUES($1,$2,$3) ON CONFLICT(id) DO UPDATE SET email=$2,name=$3",
      [u.id, u.email, u.name],
    );
  }
}

// Test implementation
class InMemoryUserRepository implements UserRepository {
  private store = new Map<number, User>();
  async findById(id: number) { return this.store.get(id) ?? null; }
  async save(u: User) { this.store.set(u.id, u); }
}

// Domain service depends only on the interface
class WelcomeService {
  constructor(private repo: UserRepository) {}
  async welcome(id: number) {
    const u = await this.repo.findById(id);
    return u ? `Hi ${u.name}` : "Unknown user";
  }
}

In tests, swap the implementation:

typescript
test("welcome", async () => {
  const repo = new InMemoryUserRepository();
  await repo.save({ id: 1, email: "alice@example.com", name: "Alice Dev" });
  const svc = new WelcomeService(repo);
  expect(await svc.welcome(1)).toBe("Hi Alice Dev");
});
bash
npx vitest run user.test.ts

Output:

text
 ✓ welcome
Test Files  1 passed (1)
     Tests  1 passed (1)

Repository is most valuable when business logic is non-trivial and you want to test it without a database. If your "service" is just CRUD passthrough, Repository adds layers without adding value — call the ORM directly.

Singleton — and why it is usually wrong

A Singleton guarantees exactly one instance of a class exists in the process. It is the most famous GoF pattern and, in 2026, the one most likely to be a code smell. Singletons act as global mutable state: they make tests order-dependent, hide dependencies, and break in multi-threaded or serverless environments where "process" is a fluid concept.

python
# DON'T — classic singleton (Python)
class Config:
    _instance: "Config | None" = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

What you almost always want instead is dependency injection: construct one instance at app startup and pass it to whoever needs it.

python
# DO — construct once, pass explicitly
def make_app(config: Config) -> App:
    repo = UserRepo(config.db_url)
    return App(config, repo)

if __name__ == "__main__":
    app = make_app(Config.load())
    app.run()

The "one instance" property is preserved (you only construct one), but each consumer declares its dependency, making tests trivial and the wiring obvious.

Legitimate Singleton-ish uses are narrow: process-wide logger registries, OS resource handles (clipboard, GPU), and language-level constants. Everything else is global state in a tuxedo.

Command

The Command pattern packages a request as an object so it can be queued, logged, retried, or undone. It is the pattern behind every "undo stack", "job queue", and "audit log". Reach for it when an action needs to outlive its caller — when you need to serialize it, schedule it, or rerun it.

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

class InsertText implements Command {
  constructor(private doc: { text: string }, private at: number, private str: string) {}
  execute() { this.doc.text = this.doc.text.slice(0, this.at) + this.str + this.doc.text.slice(this.at); }
  undo()    { this.doc.text = this.doc.text.slice(0, this.at) + this.doc.text.slice(this.at + this.str.length); }
}

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

const doc = { text: "hello" };
const h = new History();
h.do(new InsertText(doc, 5, " world"));
console.log(doc.text);
h.undo();
console.log(doc.text);
bash
node command.js

Output:

text
hello world
hello

For job queues (Sidekiq, BullMQ, Celery), the Command is serialized to JSON and replayed by a worker. The pattern's discipline — "everything an action needs is on the object" — is what makes that serialization possible.

State

The State pattern lets an object change behaviour when its internal state changes. Each state is a class implementing the same interface; the context delegates to its current state and lets each state decide what the next one is. It is Strategy with the twist that the strategies decide the transitions.

python
# Python — order lifecycle
class OrderState:
    def pay(self, ctx): raise NotImplementedError
    def ship(self, ctx): raise NotImplementedError
    def cancel(self, ctx): raise NotImplementedError

class Pending(OrderState):
    def pay(self, ctx): ctx.state = Paid(); print("paid")
    def ship(self, ctx): print("cannot ship unpaid order")
    def cancel(self, ctx): ctx.state = Cancelled(); print("cancelled")

class Paid(OrderState):
    def pay(self, ctx): print("already paid")
    def ship(self, ctx): ctx.state = Shipped(); print("shipped")
    def cancel(self, ctx): ctx.state = Refunded(); print("refunded")

class Shipped(OrderState):
    def pay(self, ctx): print("already paid")
    def ship(self, ctx): print("already shipped")
    def cancel(self, ctx): print("cannot cancel a shipped order")

class Cancelled(OrderState):  # terminal
    def pay(self, ctx): print("cancelled — no payments")
    def ship(self, ctx): print("cancelled — no shipments")
    def cancel(self, ctx): print("already cancelled")

class Refunded(OrderState):   # terminal
    def pay(self, ctx): print("refunded — no payments")
    def ship(self, ctx): print("refunded — no shipments")
    def cancel(self, ctx): print("already refunded")

class Order:
    def __init__(self): self.state: OrderState = Pending()
    def pay(self): self.state.pay(self)
    def ship(self): self.state.ship(self)
    def cancel(self): self.state.cancel(self)

o = Order()
o.ship()   # rejected
o.pay()
o.ship()
o.cancel() # rejected — already shipped
bash
python order_state.py

Output:

text
cannot ship unpaid order
paid
shipped
cannot cancel a shipped order

When the number of states or transitions grows past ~7, switch to a real state-machine library (xstate in JS, transitions in Python). They visualize, persist, and validate the graph.

Iterator

An Iterator gives sequential access to a collection without exposing its internal layout. Modern languages have it built-in (for...of, for ... in, generators), so you rarely implement one by hand — but understanding the protocol matters when you write data structures or stream large inputs.

python
# Python generator — Iterator pattern in one line
def line_iter(path: str):
    with open(path) as f:
        for line in f:
            yield line.rstrip("\n")

for line in line_iter("access.log"):
    if "ERROR" in line:
        print(line)
bash
python iter.py

Output:

text
2026-05-25 10:01:55 ERROR DB connection lost
2026-05-25 10:02:33 ERROR auth failed for user alice@example.com

Generators consume O(1) memory regardless of input size — the difference between processing a 4-line log and a 4 GB log is exactly zero in memory terms. This is the modern Iterator pattern's main payoff.

Template Method

Template Method defines the skeleton of an algorithm in a base class and lets subclasses override specific steps. It is how most testing frameworks are structured (setUp, runTest, tearDown) and how request pipelines work in many web frameworks.

python
# Python — Template Method for data import jobs
from abc import ABC, abstractmethod
import time

class ImportJob(ABC):
    def run(self) -> None:
        t0 = time.time()
        data = self.fetch()
        cleaned = self.clean(data)
        self.persist(cleaned)
        print(f"imported {len(cleaned)} rows in {time.time()-t0:.2f}s")

    @abstractmethod
    def fetch(self) -> list[dict]: ...
    def clean(self, rows: list[dict]) -> list[dict]:
        return [r for r in rows if r]
    @abstractmethod
    def persist(self, rows: list[dict]) -> None: ...

class CsvImport(ImportJob):
    def __init__(self, path: str): self.path = path
    def fetch(self) -> list[dict]:
        return [{"id": 1, "name": "Alice Dev"}, {"id": 2, "name": "Bob"}]
    def persist(self, rows: list[dict]) -> None:
        for r in rows: print("INSERT", r)

CsvImport("users.csv").run()
bash
python template.py

Output:

text
INSERT {'id': 1, 'name': 'Alice Dev'}
INSERT {'id': 2, 'name': 'Bob'}
imported 2 rows in 0.00s

When subclasses only differ in one or two methods, prefer composition (Strategy) — Template Method ties you to inheritance, which makes testing and reuse harder.

Builder

Builder constructs complex objects step by step, separating construction from representation. In statically-typed languages with optional/named arguments (Python, Kotlin, C#) the language already does most of Builder's job, so it earns its keep mainly in Java and Go where keyword arguments do not exist, or when the build order is itself meaningful (DSLs, query builders).

typescript
// TypeScript — query Builder
class QueryBuilder {
  private parts: { table?: string; where: string[]; order?: string; limit?: number } = { where: [] };
  from(t: string)            { this.parts.table = t; return this; }
  where(s: string)           { this.parts.where.push(s); return this; }
  orderBy(s: string)         { this.parts.order = s; return this; }
  limit(n: number)           { this.parts.limit = n; return this; }
  build(): string {
    const where = this.parts.where.length ? ` WHERE ${this.parts.where.join(" AND ")}` : "";
    const order = this.parts.order ? ` ORDER BY ${this.parts.order}` : "";
    const limit = this.parts.limit ? ` LIMIT ${this.parts.limit}` : "";
    return `SELECT * FROM ${this.parts.table}${where}${order}${limit}`;
  }
}

const sql = new QueryBuilder()
  .from("users")
  .where("active = true")
  .where("created_at > NOW() - INTERVAL '30 days'")
  .orderBy("created_at DESC")
  .limit(20)
  .build();

console.log(sql);
bash
node builder.js

Output:

text
SELECT * FROM users WHERE active = true AND created_at > NOW() - INTERVAL '30 days' ORDER BY created_at DESC LIMIT 20

Chain of Responsibility

A Chain of Responsibility passes a request through a list of handlers; each one chooses to act, pass, or short-circuit. It is the pattern behind every middleware stack (Express, FastAPI, ASGI) and most authorization layers.

python
# Python — middleware-style chain
from typing import Callable

Handler = Callable[[dict, Callable], dict]

def auth(req, nxt):
    if not req.get("token"): return {"status": 401, "body": "unauthorized"}
    return nxt(req)

def logger(req, nxt):
    print(f"-> {req['path']}")
    resp = nxt(req)
    print(f"<- {resp['status']}")
    return resp

def handler(req, nxt):
    return {"status": 200, "body": f"hello {req['path']}"}

def chain(*fns: Handler) -> Handler:
    def call(req, nxt=None):
        i = 0
        def step(req):
            nonlocal i
            if i >= len(fns): return nxt(req) if nxt else {"status": 404}
            fn = fns[i]; i += 1
            return fn(req, step)
        return step(req)
    return call

app = chain(logger, auth, handler)
print(app({"path": "/", "token": "abc"}))
print(app({"path": "/", "token": ""}))
bash
python chain.py

Output:

text
-> /
<- 200
{'status': 200, 'body': 'hello /'}
-> /
<- 401
{'status': 401, 'body': 'unauthorized'}

Visitor

The Visitor pattern separates operations from the data structure they operate on, so you can add a new operation without modifying the structure's classes. Use it when the structure is stable (AST nodes, file-tree items) but the set of operations grows (pretty-print, type-check, optimize, lint).

In modern languages, pattern matching does most of Visitor's job with less ceremony:

python
# Python 3.10+ — structural pattern matching as Visitor
from dataclasses import dataclass

@dataclass
class Num: value: int
@dataclass
class Add: left: "Expr"; right: "Expr"
@dataclass
class Mul: left: "Expr"; right: "Expr"
Expr = Num | Add | Mul

def evaluate(e: Expr) -> int:
    match e:
        case Num(v): return v
        case Add(l, r): return evaluate(l) + evaluate(r)
        case Mul(l, r): return evaluate(l) * evaluate(r)

def to_str(e: Expr) -> str:
    match e:
        case Num(v): return str(v)
        case Add(l, r): return f"({to_str(l)} + {to_str(r)})"
        case Mul(l, r): return f"({to_str(l)} * {to_str(r)})"

tree = Add(Num(2), Mul(Num(3), Num(4)))
print(to_str(tree), "=", evaluate(tree))
bash
python visitor.py

Output:

text
(2 + (3 * 4)) = 14

Anti-patterns and their fixes

Anti-patternWhy it hurtsReplace with
Singleton-as-globalHidden coupling, untestable, breaks in concurrencyDI: construct once, pass explicitly
God ObjectOne class knows everythingSplit by responsibility (SRP)
Anemic Domain ModelLogic outside the data; data classes onlyPut behaviour with the data
Smart UIBusiness logic in views/controllersPush logic into services
Service LocatorHidden dependency lookupConstructor injection
Implementation Inheritance for code reuseFragile base class problemComposition + interfaces
Premature Abstract FactoryLayers without payoffPlain function, add abstraction later
Magic Pattern SoupPatterns applied to look "professional"Boring code; patterns when 3 instances appear

Functional vs OOP perspective

Many GoF patterns dissolve in functional languages. The table below shows the mapping for the patterns that change shape most.

OOP PatternFunctional equivalent
StrategyHigher-order function (Callable, (x) => y)
CommandClosure or () => T thunk
ObserverReactive stream (Rx, signals, channels)
IteratorLazy sequence / generator
Template MethodHigher-order function with hook callbacks
DecoratorFunction composition (f ∘ g)
Chain of Responsibilityreduce over a list of handlers
VisitorPattern matching / catamorphism
SingletonModule-level binding (Python module, Haskell top-level value)

If your language has first-class functions and algebraic data types, prefer the functional form when each variant fits in one screen of code; reach for the OOP form when each variant has its own state, lifecycle, or external dependencies.

Common pitfalls

  • Pattern-name disease — naming a class OrderFactoryAdapter does not make it one. Patterns describe roles, not class names. Name classes after the domain (OrderRouter, LegacyPaymentGateway), not the pattern.
  • Premature abstraction — applying a pattern to a single case adds 3 layers and 0 flexibility. Wait for the third instance. The cost of refactoring later is almost always lower than the cost of the unneeded indirection now.
  • Singleton-as-globals smuggling — a Singleton is global mutable state. Tests break, ordering matters, threading is unsafe. Use dependency injection.
  • Decorator stack explosion — three layers of decorator is fine; ten is a debugging nightmare. Stop when the stack would not fit on a sticky note.
  • Mixing layers — a Repository that knows about HTTP status codes, or a Strategy that touches the database directly, breaks the abstraction. Keep each pattern's responsibility narrow.
  • Inheritance for code reuse — extending a class to "borrow" two methods couples you to its entire surface. Prefer composition; inheritance is for is-a, not has-a.
  • Visitor for unstable data — Visitor wins when the data classes are stable. If you add a new node type every sprint, you will edit every visitor — at that point, just use methods on the node.
  • State machines as if-else — once an entity has 4+ states with restricted transitions, a real state machine library prevents a class of bugs that no review will catch.

Real-world recipes

Strategy via config flag

A production-ready pattern: pick the implementation at startup, keep the rest of the code free of branching.

typescript
// src/checkout/index.ts
import { StandardPricing, BlackFridayPricing, type PricingStrategy } from "./strategies";

const STRATEGIES: Record<string, () => PricingStrategy> = {
  standard:     () => new StandardPricing(),
  black_friday: () => new BlackFridayPricing(),
};

export function pricingFor(env: { PRICING: string }): PricingStrategy {
  const make = STRATEGIES[env.PRICING];
  if (!make) throw new Error(`unknown pricing strategy: ${env.PRICING}`);
  return make();
}

Repository + Decorator stack for caching

typescript
// src/users/repo.ts
const base = new PostgresUserRepository(db);
const cached = new CachingUserRepository(base, redis, { ttlSeconds: 60 });
const logged = new LoggingUserRepository(cached);
export const userRepo: UserRepository = logged;

Domain code imports userRepo. Tests import new InMemoryUserRepository().

Factory + discriminated union

Choose Strategy or Factory based on whether the caller needs to inspect the result. Discriminated unions (TS, Rust, Swift) make Factory output exhaustively checkable:

typescript
type Result =
  | { kind: "ok"; value: User }
  | { kind: "not_found"; id: number }
  | { kind: "error"; reason: string };

function loadUser(id: number): Result { /* ... */ return { kind: "ok", value: { id, email: "a@b.c", name: "Alice Dev" } }; }

const r = loadUser(42);
switch (r.kind) {
  case "ok":         return r.value;
  case "not_found":  console.warn(`missing ${r.id}`); break;
  case "error":      console.error(r.reason); break;
}

Observer with auto-cleanup (TS / React)

typescript
// Useful for global event buses in long-lived apps
function on<T>(bus: EventEmitter, event: string, handler: (e: T) => void): () => void {
  bus.on(event, handler);
  return () => bus.off(event, handler);
}

// React: useEffect returns the cleanup
useEffect(() => on(bus, "user.created", refresh), [refresh]);

Command for an undoable editor

typescript
class History {
  private done: Command[] = [];
  private redoStack: Command[] = [];
  do(cmd: Command) { cmd.execute(); this.done.push(cmd); this.redoStack = []; }
  undo() { const c = this.done.pop(); if (c) { c.undo(); this.redoStack.push(c); } }
  redo() { const c = this.redoStack.pop(); if (c) { c.execute(); this.done.push(c); } }
}

State machine for billing

python
# transitions library — declarative state machine
from transitions import Machine

states = ["trial", "active", "past_due", "cancelled"]
transitions = [
    {"trigger": "subscribe", "source": "trial",     "dest": "active"},
    {"trigger": "fail",      "source": "active",    "dest": "past_due"},
    {"trigger": "recover",   "source": "past_due",  "dest": "active"},
    {"trigger": "cancel",    "source": ["trial", "active", "past_due"], "dest": "cancelled"},
]

class Account:
    def __init__(self): Machine(self, states=states, transitions=transitions, initial="trial")

a = Account()
a.subscribe()
print(a.state)
a.fail()
print(a.state)
a.recover()
print(a.state)
bash
python billing_fsm.py

Output:

text
active
past_due
active

Tips

A pattern is a name for a structure that already exists in your code. If you cannot point to it, you have not found the pattern yet — and forcing one in usually makes the code worse.

Cross-link: see SOLID for the principles that motivate most patterns, Testing Strategies for how Repository + Strategy make tests fast, and Code Review for spotting misapplied patterns.

Patterns are vocabulary. The win is not the structure — every team eventually writes Strategy. The win is being able to say "this is a Strategy" in standup and have your teammates picture the same code.