TRT
Sign in
System DesignUpdated

Design Patterns as Named Tradeoffs

A design pattern names a recurring structure for solving a family of design problems.

The Runtime Theory Team8 min read#design-patterns#abstraction#testing

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.

go
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:

text
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 changes

The 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 if requires 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:

text
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 loses

The 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 / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, 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:

  1. 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.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. 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.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save