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.
| Symptom | Likely pattern |
|---|---|
Big if/else or switch on a type tag | Strategy, State, or polymorphism |
| Constructor takes 7+ arguments | Builder or parameter object |
| One change ripples through many call sites | Observer or pub/sub |
| Need to wrap an old API to fit a new one | Adapter |
| Want to add behaviour without subclassing | Decorator |
| Multiple data sources behind one interface | Repository |
| Adding a new variant requires touching many files | Factory + interface |
| Step-by-step process with reusable steps | Template Method or pipeline |
| Action must be queued, undone, or logged | Command |
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 — 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:
140
In Python, Strategy collapses to a function-typed parameter — the interface is the function signature:
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))
python checkout.py
Output:
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 — 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")
python events.py
Output:
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 — 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");
NOTIFIER=sms node notifier.js
Output:
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 — 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")
python adapter.py
Output:
[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 — 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")
python cache_decorator.py
Output:
first: 0.50s, second: 0.0001s
For class-based decoration — the "wrap and forward" pattern — the structure is uniform:
// 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
UserServicedoing 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 — 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:
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");
});
npx vitest run user.test.ts
Output:
✓ 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.
# 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.
# 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 — 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);
node command.js
Output:
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 — 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
python order_state.py
Output:
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 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)
python iter.py
Output:
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 — 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()
python template.py
Output:
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 — 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);
node builder.js
Output:
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 — 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": ""}))
python chain.py
Output:
-> /
<- 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 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))
python visitor.py
Output:
(2 + (3 * 4)) = 14
Anti-patterns and their fixes
| Anti-pattern | Why it hurts | Replace with |
|---|---|---|
| Singleton-as-global | Hidden coupling, untestable, breaks in concurrency | DI: construct once, pass explicitly |
| God Object | One class knows everything | Split by responsibility (SRP) |
| Anemic Domain Model | Logic outside the data; data classes only | Put behaviour with the data |
| Smart UI | Business logic in views/controllers | Push logic into services |
| Service Locator | Hidden dependency lookup | Constructor injection |
| Implementation Inheritance for code reuse | Fragile base class problem | Composition + interfaces |
| Premature Abstract Factory | Layers without payoff | Plain function, add abstraction later |
| Magic Pattern Soup | Patterns 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 Pattern | Functional equivalent |
|---|---|
| Strategy | Higher-order function (Callable, (x) => y) |
| Command | Closure or () => T thunk |
| Observer | Reactive stream (Rx, signals, channels) |
| Iterator | Lazy sequence / generator |
| Template Method | Higher-order function with hook callbacks |
| Decorator | Function composition (f ∘ g) |
| Chain of Responsibility | reduce over a list of handlers |
| Visitor | Pattern matching / catamorphism |
| Singleton | Module-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
OrderFactoryAdapterdoes 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, nothas-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.
// 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
// 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:
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)
// 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
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
# 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)
python billing_fsm.py
Output:
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.