all_lessons / system_design / 14 · optimisation lesson 14 / 19

Optimisation playbook

The earlier lessons each dropped an optimisation as an aside — cache the read path, batch the calls, pick the right index, shed the fan-out. This lesson gathers them into one menu and, more importantly, teaches the discipline that decides which one to reach for. The headline: optimisation is not "make code fast," it is "find the dominant term and make it smaller — then re-measure." Most engineers lose weeks optimising the wrong term.

First principle

A system's total cost (latency, money, load) is a sum of terms. Speeding up any term yields a return proportional to how big that term already is. So the entire game reduces to: (1) measure to find the biggest term, (2) attack it with the cheapest lever that shrinks it, (3) re-measure, because the bottleneck has now moved. The cheapest win is almost always doing less work, not doing the same work faster.

The discipline: measure, attack the dominant term, repeat

Before any technique, the method. Optimisation done by intuition is gambling — the part of the code you feel is slow is routinely not the part the clock agrees is slow. So the cardinal rule is a loop, not a step:

┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ │ MEASURE │ ──► │ ATTACK the │ ──► │ RE-MEASURE │ │ profile, │ │ dominant term │ │ did it move? │ │ find the │ │ (cheapest lever │ │ bottleneck │ │ biggest term │ │ that shrinks it) │ │ has MOVED ──┐ │ └─────────────┘ └──────────────────┘ └─────────────┼─┘ ▲ │ └──────────────────────────────────────────────────┘

Three corollaries you should be able to recite:

Amdahl's law — why the dominant term is the only one worth chasing

Start with numbers, not algebra. A request takes 100 ms: 60 ms in the database, 40 ms everything else. Suppose you make that "everything else" infinitely fast — zero. You are left with 60 ms. You worked miracles on 40% of the budget and the request is still 60 ms: a 1.67× speedup, and that is the best you could ever do by touching only that slice. Conversely a mere 2× on the 60 ms DB slice (→ 30 ms) gives 100/70 = 1.43× — already comparable, from far less heroics.

Now the formula. If a fraction p of the time can be sped up by a factor s, the overall speedup is:

speedup = 1 / ((1 − p) + p/s)

Push s → ∞ (make that slice free) and the p/s term vanishes, leaving the hard ceiling:

max speedup = 1 / (1 − p)

That ceiling is the whole lesson in one line: you cannot speed the system up by more than the share of time you are actually attacking allows, however brilliant the optimisation. Optimising a 5% slice caps you at 1/(1−0.05) ≈ 1.05× — a rounding error. The intuition: chase the big slice.

Fraction p you optimiseCeiling 1/(1−p) (even at s=∞)Speedup at a realistic s=4×Reading
0.05 (a 5% slice)1.05×1.04×not worth touching
0.201.25×1.19×marginal
0.502.0×1.60×worth it
0.805.0×2.5×the bottleneck — attack here
0.9520×3.48×huge leverage

Amdahl is also why the loop never ends: once you halve the 80% slice it becomes ~67% of a smaller total, and some other term is now the dominant one. The bottleneck moves; you re-measure and chase the new biggest slice.

The optimisation ladder — cheapest, highest-leverage first

Climb this top-down. Each rung is harder and riskier than the one above, so exhaust the cheap rungs before reaching for concurrency or more machines. For every rung: what it buys, what it costs.

Rung 1 — Do less work (the highest-leverage rung)

The work you skip is free and never fails. This is where the biggest, cheapest wins live.

Rung 2 — Move work off the hot path

If work must happen but the user needn't wait for it, get it off the critical path. This shrinks p for the synchronous request even though total work is unchanged.

Rung 3 — Reduce round trips & bytes moved

The latency ladder (01) is the reason this rung matters: one network round trip (~0.5–150 ms) dwarfs a thousand memory reads (~100 ns each). A request that makes ten sequential hops has already lost the game before any code runs.

Rung 4 — Make each unit of work faster

Only now do we make the work itself faster — the rung most people start on, and usually too early.

Rung 5 — Parallelise / scale out

When the work is irreducible and already lean, do it in parallel or on more machines. Powerful, but the most expensive rung and the one with a sharp tail-latency trap.

Rung 6 — Right storage tier

Match where data lives to how hot it is, using the latency ladder (01): RAM ≫ SSD ≫ spinning disk ≫ network. Keep the working set (the data actually touched) in RAM; let cold data fall to cheaper tiers. Buys: orders-of-magnitude latency on hot data. Costs: RAM is expensive; tiering adds complexity.

The techniques, consolidated

One table to scan in an interview. Note every row names a cost — a technique without a stated cost is a technique you don't yet understand.

TechniqueWhat it buysWhat it costs / when it backfires
Caching (06)Latency + backend load collapse on hot readsStaleness, invalidation, a new failure tier; useless for write-heavy or unique reads
Precompute / materialise (19)Reads become cheap lookupsWrite amplification; stale views; rebuild cost
Avoid N+1 / batch queries (03)N−1 round trips removedFatter queries; over-fetching if careless
Async / background jobs (11)Tail work off the hot path; burst absorptionEventual consistency; a queue to run; debugging
Batching / pipelining (02)Throughput, hardware efficiencyPer-request latency & variance rise
Fewer hops / collapse RTTs (01)Each removed round trip is pure latency savedService coupling, coarser boundaries
Projection / compression / binary (03)Fewer bytes moved & parsed(De)serialise CPU; less human-readable
Right index (04)Scan → seek, O(N) → O(log N)Slower writes, storage per index; wrong index unused
Better algorithm / data structureLower asymptotic cost per callComplexity; sometimes more memory
Connection pooling / keep-aliveTCP/TLS setup amortised awayPool sizing; exhaustion stalls the app
Parallelise / fan-outWall-clock latency, throughputTail-at-scale: more fan-out → worse p99
Horizontal scale (05)Lower ρ per node; capacity$, coordination, partitioning complexity
Hotter storage tier (01)Orders-of-magnitude on hot dataRAM cost; tiering complexity

The traps

Five ways optimisation goes wrong

Try it: an Amdahl latency-budget optimiser

A request's 100 ms budget is split across four components. Pick one to optimise, set a speedup factor, and watch the overall speedup — and the Amdahl ceiling for that slice (the best you could ever do at infinite speedup). The lesson lands when you optimise the 5 ms serialisation slice 20× and barely move the total, then put a modest 2× on the DB slice and save real milliseconds.

Amdahl latency-budget optimiser
overall speedup = 1 / ((1−p) + p/s), where p = the chosen slice's share of the budget. The ceiling 1/(1−p) is the most that slice can ever give you, even at s = ∞. Fixed budget: DB 60ms · network 25ms · compute 10ms · serialisation 5ms = 100ms.
Original total
New total
Overall speedup
Amdahl ceiling 1/(1−p)
Verdict

Interview prompts you should be ready for

  1. "This endpoint is slow. Where do you start optimising?" (senior answer: I don't guess — I measure first. Profile or trace the request (16) to find the dominant term, then attack that term with the cheapest lever that shrinks it, then re-measure because the bottleneck moves. The fastest win is usually doing less work, not faster work.)
  2. "How do you optimise a slow endpoint without guessing?" (senior answer: break the latency budget into terms — DB, network, compute, serialisation — via a trace; the largest term is the target. Amdahl says effort on a small term is wasted, so I confirm the slice is big before touching it, change one thing, and re-measure. Guessing is how teams burn weeks on a 5% slice.)
  3. "What's the ceiling on optimising component X if it's only 20% of latency?" (senior answer: 1/(1−p) = 1/(1−0.2) = 1.25× — even if I make X infinitely fast. So at best the request gets 25% faster; if I need more, I must attack a bigger slice. That ceiling is exactly why I measure shares before optimising.)
  4. "When is caching the wrong tool?" (senior answer: when reads aren't repetitive (low hit rate, unique keys — you pay for a cache that never hits), when the workload is write-heavy, or when the data must be strongly fresh — then a cache trades a correctness bug for a latency win. Cache buys speed with staleness; if staleness isn't affordable, reach elsewhere (06).)
  5. "We added more parallel fan-out and p99 got worse — why?" (senior answer: tail-at-scale (02). A request waiting on all n backends sees the max of n latencies, so more fan-out means a higher chance some leaf is slow; p99 climbs with n. Fixes: shallower fan-out, hedged/backup requests so the max collapses toward the median, timeouts with partial results.)
  6. "Engineer A wants to rewrite a hot function in C; the trace shows a 60 ms DB query and 2 ms in that function. What do you say?" (senior answer: that's micro-tuning a non-bottleneck. The function is ~2% of the budget — ceiling ~1.02×. The DB query is 60%, ceiling 2.5×; a single right index or removing an N+1 there saves far more than any rewrite. Attack the dominant term.)
  7. "Give the optimisation ladder from cheapest to most expensive." (senior answer: do less work (cache, precompute, kill N+1, cap pages) → move work off the hot path (async, batch) → reduce round trips and bytes (collapse hops, projection, compression) → make each unit faster (index, algorithm, pooling) → parallelise/scale out (mind the tail) → right storage tier (working set in RAM). Exhaust the top rungs first.)
  8. "You optimised the bottleneck and it's still slow — now what?" (senior answer: re-measure. Halving the dominant term shrinks the total and promotes some other term to dominant — the bottleneck moved. Optimisation is a loop: measure, attack, re-measure, until the budget meets the SLO or the next term costs more to fix than it's worth.)
Takeaway

Optimisation is a discipline, not a bag of tricks: measure, attack the dominant term, re-measure. Amdahl gives the organising law — your reward is capped at 1/(1−p), the share of time you actually attack, so chase the big slice and never the 5% one. Climb the ladder cheapest-first: do less work, then move work off the hot path, then cut round trips and bytes, then speed each unit, and only then parallelise or scale out — every rung naming its cost. The traps are all the same mistake in disguise: optimising something that isn't the bottleneck. Next, 15 · Rate limiting & the API edge — where the cheapest optimisation of all, refusing work you shouldn't do, lives at the front door.