A design pattern names a recurring structure for solving a family of design problems. Its value is communication: "this is a Strategy" transcribes roles, forces, and trade-offs in four words. A pattern is not a rule to apply whenever its name sounds familiar — it is a record of a trade-off someone else already paid to learn.
The structure under the name
Take Strategy. The recurring problem: several algorithms are interchangeable, and callers should pick among them without editing the code that uses them. The structure: an interface owned by the consumer, concrete implementations, and a selection policy kept separate from the execution.
type QuoteStrategy interface {
Quote(ctx context.Context, r Shipment) (Money, error)
}
// selection policy — the part that changes for business reasons
func carrierFor(r Shipment) QuoteStrategy {
if r.WeightKg > 30 {
return FreightStrategy{} // trucks, no air surcharge
}
if r.Destination.IsDomestic() {
return FlatRateStrategy{}
}
return CarrierAPIStrategy{FedEx: fedex, DHL: dhl} // lowest of two quotes
}
func Price(r Shipment) (Money, error) {
return carrierFor(r).Quote(ctx, r)
}What this buys is testability that a conditional cannot match: each strategy gets its own
fast unit test with its own fixtures, and the selection policy gets a table test of its
decisions — so a pricing bug is localized to one of two test files instead of a
fifteen-branch switch.
The cost ledger
Every pattern charges for what it buys, and the charge is paid in reading, not in runtime:
two algorithms, one call site, no expected growth
conditional : 1 file, ~15 lines, path readable top to bottom
strategy interface : 4 files (interface, 2 impls, selector), ~60 lines,
jump-to-definition to find each body
three or more algorithms, independently tested, independently deployed
conditional : every new rule edits the shared function — merge
conflicts, shared regression risk, one giant test file
strategy interface : new rule = new file, no existing file changesThe crossover is a real, countable thing: how many places does the next likely change
touch, with and without the structure? If the count is the same, the indirection is pure
reading cost. If the conditional grows an if per change in a shared function while the
interface adds an isolated file, the pattern has already paid for itself.
When patterns fail
- Pattern tourism. Adding an abstract factory for a single product, or Observer for two subscribers, is ceremony with no variation behind it. The test for a seam: name the second implementation that justifies it. If you can't, delete it.
- Patterns that fight the language. Strategy in Go or Java earns its keep because interfaces are explicit. In a language with first-class functions and closures, a "Strategy" that is one method long is a function argument — wrapping it in a class hierarchy adds files, not capabilities.
- Hiding control flow past the point of readability. If understanding a single
ifrequires opening five files and stepping through three dispatchers, the pattern has made the common case harder to serve the rare case. - Patterns as permanent decisions. A pattern is easy to add and annoying to remove; the removal cost is why teams keep abstractions long after the variation disappears.
Run a design review with them
Patterns are most useful in review, as shared vocabulary for arguing about a specific pressure. Compare two designs by naming the force each resolves:
design A: `switch (carrier) { ... }` inside the pricing function
design B: QuoteStrategy + separate selection policy
force: "a third pricing rule lands next quarter"
A → edits the shared function (regression risk in everything it serves)
B → adds one file
force: "pricing must deploy independently of checkout"
A → impossible: same artifact
B → possible once pricing is its own module
force: "reviewer must understand the price path in 5 minutes"
A → one function reads linearly ← A wins
B → four hops of indirection ← B losesThe review's job is to decide which force is real today. That is the check question answered directly: with two rules and a third expected, the conditional wins until the third rule (or independent testing, or per-tenant selection, or runtime swapping) actually arrives — the interface becomes worthwhile the moment one of those requirements is written down, and not before.
Key takeaways
- A pattern is a named trade-off with its roles pre-specified; use it to compress a design conversation, not to decorate a codebase.
- Keep the selection policy separate from the implementation — that split is what buys independent tests.
- Pay the reading cost only when a measured force justifies it: count the files the next likely change touches, with and without the structure.
- Patterns are easy to add, hard to remove; the asymmetry is the dominant cost.
- In review, name the force. "Because it's a pattern" is not a force.
Refactoring.Guru: Strategy Pattern walks the structure and its siblings with the same role-by-role framing.
Production cost model and latency numbers
To apply Design Patterns as Named Tradeoffs effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:
| Operation / Boundary | Typical Latency | Dominant Bottleneck | Production Mitigation |
|---|---|---|---|
| L1 / L2 Cache Access | 1 ns – 4 ns | Register dependency chains & pointer chasing | Contiguous memory layouts, cache-line alignment (64 B), and batch iteration |
| Main Memory (DRAM) Fetch | 60 ns – 100 ns | TLB misses, random heap indirection, NUMA hops | HugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers |
| Kernel Syscall / Context Switch | 1 µs – 5 µs | User-to-kernel mode switch, page-table flush, lock contention | Non-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O |
| NVMe SSD Random Read / fsync | 50 µs – 500 µs | Block I/O queue depth, write amplification, WAL flushes | Group commit, sequential append logs, and bounded buffer-pool pinning |
| Intra-Region RPC Roundtrip | 0.5 ms – 2.5 ms | Serialization, TCP/TLS framing, tail queueing ($L = \lambda W$) | Connection pooling, deadline propagation, and p95 hedged requests |
Failure modes, debugging, and operational checklist
In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on Design Patterns as Named Tradeoffs, walk through three verification questions:
- What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
- How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized
100 msslowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early. - Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.