The Backend Ladder · Deep dive · Pillar A of three
The overview page named the six mechanisms. This one opens them up: the laws that decide how much traffic a box can take, the arithmetic behind a cache, the write paths under an index, and the exact shapes failure takes when a system stops coping. Every section is a mechanism drawn, then the numbers that make it real.
Three quantitative tools do most of the work in backend design. Knowing them turns "it feels slow" into "we are at 85% utilisation on a single-threaded resource, so the wait is six times the service time".
Every design argument is really a claim about which of these numbers you are paying. They span nine orders of magnitude, so intuition fails — you have to know roughly where each operation sits.
Work in progress equals arrival rate times time in system: L = λW. It holds for any stable system, with no assumptions about distributions. Thread pools, connection pools, queue depths and pod counts all fall out of it.
How many workers? Throughput target × per-item time. 500 msg/s at 200 ms each needs 100 concurrent consumers. How deep is my queue allowed to be? Depth ÷ drain rate = added latency; a 10,000-item queue draining at 500/s is 20 seconds of delay, so if your SLO is 2s the bound is 1,000. Why did connections run out? Because W grew, not because λ did.
Waiting time does not grow linearly with load. For a single queued resource the wait multiplier is 1/(1−ρ), which is flat and forgiving until it is neither. Capacity planning is really about staying left of the knee.
Two things go wrong with concurrent code: threads disagree about what memory says, and threads queue behind each other. The first is a correctness problem with a precise rule; the second is a scaling problem with a precise curve.
A write by one thread is not automatically a write the others can see. Compilers reorder, CPUs buffer, caches lag. Correctness comes from establishing an edge — a synchronising operation that both sides participate in.
Adding workers buys less than you expect (Amdahl) and eventually buys negative throughput (the Universal Scalability Law). The second effect surprises people, and it is the reason a bigger instance sometimes performs worse.
Throughput flat while CPU climbs → contention. Throughput falling as you add pods or threads → coherency: you are paying more to coordinate than you gain. Lock wait time and context-switch rate are the two counters that separate them.
Shrink the critical section, shard the lock by key, or remove the sharing entirely with a single-writer-per-partition design. Faster locks buy you a constant factor; removing the shared point changes the shape of the curve.
Asynchronous systems trade one hard problem (availability coupling) for two others: what "delivered" means, and what happens when step three of five fails after steps one and two already committed.
There is no magic in "exactly once". The three semantics differ only in the order of two operations — acknowledging the message and doing the work — plus, optionally, a record of what you have already done.
A distributed transaction cannot roll back — steps one and two are already committed in other systems. A saga replaces rollback with compensation: a new, forward action that undoes the business effect of an earlier one.
Two things are worth knowing beyond "store the answer": the arithmetic that tells you what a hit ratio is actually worth, and the exact interleavings that leave a wrong value sitting in the cache.
Effective latency is h · Lhit + (1−h) · Lmiss, and origin load is (1−h) · λ. Both are dominated by the miss term, which is why the last percent of hit ratio is worth more than the first ninety.
Cache-aside has one interleaving that quietly stores a value that was already stale when it was read. It is rare per request and certain at scale, and no amount of careful ordering in the writer removes it.
Every storage engine spends the same three currencies: read work, write work, and space. You cannot minimise all three, and knowing which one a design is spending explains most of its behaviour under load.
Amplification is the ratio between the work you asked for and the work the disk actually did. A B-tree pays it on writes; an LSM pays it on reads and on background compaction; an index pays it in space and on every write to the table.
Each secondary index is another structure to update on every insert and on any update touching its columns. Six indexes on a hot table means one logical write becomes seven physical ones, at random offsets. When write latency creeps up with no code change, count the indexes that were added "just for that one report".
Three mechanisms decide whether an overloaded system degrades or collapses: whether pressure propagates backwards, whether deadlines shrink as they travel, and whether the failure sustains itself after the trigger is gone.
Backpressure is the property that a slow component makes its caller slow down, all the way to the entry point. Its absence is not neutral — unbounded buffers convert a throughput shortfall into unbounded latency and then into an out-of-memory kill.
Two numbers travel down a call tree and must be managed explicitly: the time remaining, which shrinks at every hop, and the probability of success, which multiplies at every hop.
The hardest outages have this shape: a trigger pushes the system into a state that sustains itself, so the system stays down after the trigger is gone. Adding capacity does not fix it, and restarting often makes it worse.
One worked example that uses every tool on this page. This is the shape of answer a systems-design interview is looking for, and the shape of a capacity document that survives review.
The brief: an order-read API, 3,000 requests per second at peak, p50 of 40 ms and p99 of 120 ms, backed by Postgres and a Redis cache. How many pods, how many connections, what timeouts, and what breaks first?
What is the queue? Every stage has one, even when it is only a connection pool. How full is it? Utilisation, not capacity, sets latency. What happens when the thing behind it slows down? In-flight work grows by Little's Law until something bounds it. Does the failure sustain itself? If retries and cold caches can feed it, you need a shedding lever, not a bigger cluster.