Skip to the chapter
←System DesignGGroundworkSDSystem Design
System Design/Beginner/B5
5 / 36 written chapters
/

The cover
  1. B1The mental model
  2. B2Client-server basics
  3. B3Vertical vs horizontal
  4. B4Databases in design
  5. B5Caching fundamentals
  6. B6APIs & communication
  7. B7Walkthrough: URL shortener
  1. I1Load balancing in depth
  2. I2CDNs
  3. I3Message queues & async
  4. I4Consistency models
  5. I5Replication
  6. I6Rate limiting & throttling
  7. I7Designing for availability
  8. I8Storage systems
  9. I9Search systems
  10. I10Microservices
  11. I11Security basics
  12. I12Walkthrough: feed/chat
  13. I13Walkthrough: notifications
  1. A1CAP theorem in depth
  2. A2Transactions and isolation
  3. A3Storage engines
  4. A4Sharding at scale
  5. A5Time, ordering and IDs
  6. A6Distributed consensus
  7. A7Batch & stream processing
  8. A8Fault tolerance
  9. A9Observability at scale
  10. A10Release safety
  11. A11Capacity estimation
  12. A12Case studies
  13. A13Walkthrough: key-value store
  14. A14Walkthrough: web crawler
  15. A15Walkthrough: payments
  16. A16Tradeoff thinking

SDB5 · Part 1 · Beginner

Caching fundamentals

A cache is one bet placed at six distances — and the interesting number is never the hit rate, it's the miss rate.

24 min read3 diagrams9 sections0 exercises
5 of 36

In this chapter

  • Every cache is the same bet, made at a different distance
  • What each layer actually buys you
  • Read strategies: cache-aside vs read-through
  • Write strategies, and when each is right
  • Eviction: what happens when memory runs out

and 4 more

about 24 min
09On this page
  1. Every cache is the same bet, made at a different distance
  2. What each layer actually buys you
  3. Read strategies: cache-aside vs read-through
  4. Write strategies, and when each is right
  5. Eviction: what happens when memory runs out
  6. Invalidation is hard, and here is exactly why
  7. Thundering herd, and the five ways to stop it
  8. Hit-rate arithmetic: why 90% is not a good number
  9. Recognizing it in an unseen problem

Every cache is the same bet, made at a different distance

A cache exists because of exactly one wager: this answer will be asked for again before it changes. Every caching layer in every architecture is that same bet — the layers differ only in how far from the user the copy sits, who pays when the bet is wrong, and how hard it is to take the copy back. Interviewers ask about caching constantly, not because inserting a Redis is hard, but because it is the fastest way to find out whether you reason about a system in rates and distributions or in adjectives. A candidate who says "we'll add a cache" and a candidate who says "at a 90% hit rate this still sends 50k reads a second to the primary, so 90% isn't the goal" are two very different hires.

one request, six chances to never reach disk browser CDN edge reverse proxy app memory Redis tier DB buffer pool 0 ms 10-30 ms 1-3 ms ~100 ns 0.3-1 ms 0.1-1 ms SSD 0.1-1 ms each layer removes work from every layer to its right the leftmost hit saves the most work — and is the hardest to invalidate a full miss pays every hop, not just the last one
The numbers are what the asker pays, so they are not one monotonic scale: the first two are the user's network distance, the rest are your servers' lookup costs (an in-process hit at ~100 ns beats a CDN hit, but it saves no user network time). What does move monotonically is the asymmetry: the further left a hit lands, the more work it removes, and the harder it is to revoke a stale copy.

What each layer actually buys you

LayerCachesTypical TTLWho can invalidate itWhat it really buys
Browser / HTTP cacheStatic assets, GET responses, service-worker dataMinutes to a year (fingerprinted assets)Nobody. Once served, it is gone until it expires or the URL changesRemoves the request entirely — no network, no server work at all
CDN edgeAssets, whole pages, API GETs, imagesSeconds to daysYou, via purge — propagation is sub-second to a few seconds on modern CDNs (Cloudflare reports a p50 under 150 ms for tag purge); budget tens of seconds for slow or tiered setupsCuts a distant user's RTT (70-230 ms cross-region) to a nearby edge's 10-30 ms, and can absorb 80-99% of requests for highly cacheable content before they reach your region
Reverse proxy (nginx, Varnish, Envoy)Rendered fragments, upstream responsesSeconds to minutesYou, instantly — it's your boxShields origin from duplicate work; the natural home for request coalescing
In-process (Caffeine, an LRU Map)Config, feature flags, hot rows, compiled templatesSeconds to minutesOnly that process; every instance has its own copy~100 ns lookups, zero network — but N instances means N copies and N stale windows
Shared cache tier (Redis, Memcached)Objects, session state, computed aggregates, rate-limit countersSeconds to hoursYou, atomically, for the whole fleetOne consistent copy across all app servers; the workhorse layer
Database buffer poolPages the storage engine recently touchedUntil evictedNobody — and you don't want toFree, already on; the reason "the database is slow" is usually false for hot data

The buffer pool is the layer candidates forget exists, and it changes the conversation. A Postgres box with 64 GB of RAM and a 40 GB hot set is already serving nearly every read from memory. Adding Redis in front of it does not save you a disk seek — it saves you connection setup, query parsing, planning, MVCC visibility checks and result serialisation. That is often still a several-fold latency win (workload-dependent; measure it), but say why, because "the database reads from disk" is frequently wrong and an interviewer who runs databases will notice.

Say it like this → "I'd cache at two layers here. CDN for the read-only public content, because that removes traffic from my region entirely, and a shared Redis tier for per-user objects, because those need one consistent copy across the fleet. I'd deliberately skip a per-instance in-memory cache for anything mutable — N instances means N independent stale windows and no way to purge them."

Read strategies: cache-aside vs read-through

Cache-aside (also "lazy loading") puts the application in charge: ask the cache, and on a miss go fetch and populate it yourself. This is the one you will be asked to write on a whiteboard, so write it well — the three-line version everyone produces has three real bugs in it.

async function getUser(id) {
  const key = `user:v3:${id}`;                // v3 = payload schema version; see invalidation
  const hit = await redis.get(key);

  // null is the miss signal. Test for it explicitly. The classic bug is testing
  // the *deserialised* value for truthiness (or a client that returns raw
  // values): a legitimately cached 0, "" or false then looks like a miss.
  if (hit !== null) return JSON.parse(hit);

  const row = await db.users.findById(id);    // the miss path — the expensive part

  if (row === null) {
    // Negative caching. Without it, a scraper hitting nonexistent ids
    // passes straight through the cache into the database, every time.
    await redis.set(key, "null", "EX", 30);
    return null;
  }

  // Jittered TTL: 300 s ± 30. A million keys populated in the same minute
  // must not expire in the same second. This one line prevents avalanche.
  const ttl = 300 + Math.floor(Math.random() * 61) - 30;
  await redis.set(key, JSON.stringify(row), "EX", ttl);
  return row;
}

Note what this function does not do: it never writes to the cache on the write path. The write path deletes the key and lets the next read repopulate it. That asymmetry is deliberate and we'll see why in the invalidation section.

Read-through moves that same logic behind the cache client, so the application only ever talks to the cache and the cache knows how to load a miss. Cleaner code, one place to implement coalescing and metrics — but the cache is now on the critical path for correctness, not just speed. With cache-aside, Redis being down can degrade to slow, because the application can fall through to the database; with read-through, Redis being down means down unless the cache layer itself has a bypass. But "degrades to slow" only holds while the database can absorb the full read load, and at a 99% hit rate it usually cannot (see the node-loss box below). So the real answer when an interviewer asks which you'd pick is: cache-aside gives you the option of failing open, and you still need load shedding to make that option survivable.

Write strategies, and when each is right

StrategyWhat the write doesCostFailure modeReach for this when…
Cache-aside + invalidateWrite DB, then DEL the keyNone on the write pathA read/write interleaving can leave a stale entry until TTLThe default. Pick this unless you can name why not
Write-throughWrite cache and DB synchronously, both must succeedEvery write pays both latenciesCaches data that may never be read; a cache outage stalls writesThe same key is read within seconds of being written (profile edit, cart update) and write latency budget is loose
Write-behind (write-back)Write cache, ack the client, flush to DB in batches laterDurability — the ack is a lie until the flush landsCache node dies with unflushed writes; ordering across keys is hardHigh-volume, low-value-per-write, coalescable data: view counts, likes, "last seen", metrics
Write-aroundWrite DB only, never touch the cacheFirst read after a write is always a missNothing, which is the pointBulk imports, logs, audit rows — write-once data that would otherwise evict your hot set
Refresh-aheadProactively recompute an entry before its TTL expiresWasted work on keys nobody asks for againAmplifies load if applied to a large keyspaceA small, known set of extremely hot keys — a homepage feed, a leaderboard, a config blob

Write-behind is the one worth volunteering. A "like" counter taking 50k increments a second is 50k row updates a second with lock contention on a single hot row — a database will simply refuse. Buffer the increments in Redis, flush the delta every second, and 50k writes become one. You have traded "we might lose the last second of counts if a Redis node dies" for a 50,000× reduction in write load, and for a like counter that is obviously the right trade. Say the trade out loud; that is the whole point of the answer.

Eviction: what happens when memory runs out

TTL is expiry — the entry becomes invalid at a known time. Eviction is what the cache does when it is full and someone wants to write anyway. They are different mechanisms and conflating them is a tell.

PolicyKeepsBreaks onReach for this when…
LRURecently accessed keysScans — one analytics query touching a million cold rows flushes your entire working setGeneral purpose, access is recency-correlated (sessions, recent items)
LFUFrequently accessed keysAging — yesterday's viral post keeps a slot forever unless counters decayA stable long-tail popularity distribution; scan resistance matters
FIFO / randomNothing in particularNothing badly, surprisinglyYou need O(1) with zero bookkeeping; random is a sensible choice when keys are accessed roughly uniformly (Redis docs: a repeating cycle), and worse than LRU on skewed workloads
TTL-only (volatile-ttl)Entries furthest from expiryKeys written without a TTL — they become unevictableEvery entry genuinely has a natural lifetime
W-TinyLFU (Caffeine)Whatever a frequency sketch says earns its slotVery little; near-optimal hit ratesIn-process JVM caches where the extra few percent of hit rate is worth a real library

Two implementation details worth knowing because they get asked. First, Redis does not implement true LRU: it samples a handful of keys (maxmemory-samples, default 5) and evicts the least recently used of the sample. Redis's docs describe it as a close approximation of true LRU (a sample size of 10 gets very close, at some extra CPU), and minimal or no difference on power-law access, for much less bookkeeping memory. It is a nice example of the approximate-is-fine reasoning these systems are built on. Second, Redis LFU counters are 8-bit probabilistic counters with logarithmic increment and time-based decay — you cannot store a true frequency for a billion keys, so you store something that ranks them well enough and costs one byte.

Dry run: an LRU cache, capacity 3, through five operations

#OperationResultCache after (MRU → LRU)
1GET amiss — load, insert[a]
2GET bmiss — load, insert[b, a]
3GET cmiss — load, insert (cache now full)[c, b, a]
4GET ahit — a is promoted to most-recently-used[a, c, b]
5GET dmiss — cache full, evict the LRU entry (b), insert d[d, a, c]

Step 5 is the whole policy in one row: b was the least recently touched, not the oldest — a was inserted before it but survives because a hit re-promotes it. LRU tracks recency of access, not age, which is exactly why it breaks on a single cold scan: a query that touches a million rows once each promotes all of them ahead of your actually-hot keys.

⚠ Running a datastore under an eviction policy Sessions, idempotency keys, rate-limit state and distributed locks are frequently parked in "the Redis" — which is configured allkeys-lru because it is a cache. Under memory pressure it will silently evict a session and log a user out, or evict an idempotency key and let a duplicate payment through. Anything whose loss is a correctness bug belongs in a separate instance set to noeviction, where a full memory condition fails writes loudly instead of corrupting state quietly.

Invalidation is hard, and here is exactly why

"There are two hard things in computer science" is a joke everyone repeats and almost nobody unpacks. The concrete reason is this: there is no transaction that spans your cache and your database. They are two independent systems, so any two operations against them can interleave, and one specific interleaving is permanently damaging.

reader A writer B GET key → miss SELECT → v1 UPDATE → v2 committed DEL key → nothing there SET key = v1 (stale) A is holding v1 across this entire window t0 t1 t2 t3 t4 the cache now serves v1 until the TTL expires — the delete already happened
Nothing failed and nobody wrote buggy code. The reader simply held a value across a window in which the world changed, and then persisted it. Every cache-aside deployment has this race; the only question is how long it can last.

Once you can draw that, the practical patterns stop being folklore and become answers to a specific question — how long can a stale entry survive?

  • TTL. The honest answer. It does not prevent staleness, it bounds it. A 60-second TTL means the worst case above resolves in at most 60 seconds. Most systems are correct because of TTL, not because of clever invalidation, and saying so is a sign of experience rather than a concession.
  • Delete, never update. On write, DEL the key; do not compute the new value and SET it. Two concurrent writers who both SET can land in either order and the loser's value sticks. Two concurrent writers who both DEL converge on "empty", and the next read repopulates from the committed source of truth.
  • Versioned keys. The strongest pattern. Never mutate an entry — put the version in the key: user:42:v17, where 17 comes from a row counter, an ETag, or the row's updated_at. A write bumps the version, so new readers ask for a key nobody has written stale data into. The race above becomes harmless provided readers learn the current version from an authoritative source: A's stale SET lands on v16, which no reader asks for once they know the current version is 17. The catch is that "learn the current version" is itself a read of authoritative state (a cheap version-column or ETag lookup). If you cache that version pointer, you have moved the staleness problem up one level rather than removed it. Old entries cost memory until eviction reaps them.
  • Explicit purge / surrogate keys. What CDNs give you: tag a response product:42 and purge every edge copy carrying that tag in one call. Essential at the CDN layer, but remember purge is itself a distributed system — it is fast on modern CDNs (sub-second to a few seconds) and it can still fail or lag.
  • CDC-driven invalidation. Tail the database's write-ahead log (Debezium and friends) and emit invalidations from there. This is the only approach where invalidation is derived from committed state, in commit order, with no dual write. It costs you a pipeline; buy it when correctness matters more than simplicity. The principle behind it: treat the cache as derived data built from one authoritative write, not as a second place you also write (a dual write, which can diverge on any partial failure).
  • Leases. Facebook's memcache fix for exactly the race above: a miss returns a lease token to the one reader allowed to repopulate, and a delete invalidates outstanding tokens, so the stale SET from reader A is rejected. The same token rate-limits stampedes (see the Facebook paper in the sources).
The one that dissolves the problem The only cache you never have to invalidate is one whose key contains the identity of its content. Fingerprinted asset URLs, versioned cache keys and content-addressed blobs are all the same trick: make the new value live at a new address, and staleness becomes impossible rather than merely short-lived.
⚠ The layer you cannot take back You can purge Redis in a millisecond and a CDN in about a second or a few, but a response you served with Cache-Control: max-age=3600 is sitting in a browser you will never speak to again for the next hour. This is why HTML gets no-cache or a short max-age while fingerprinted JS and CSS get immutable, max-age=31536000. Get that backwards and a bad deploy is unfixable for everyone who loaded the page.

Thundering herd, and the five ways to stop it

A single key serving 50,000 requests a second expires. Recomputing it takes 20 ms. In that window 50,000 × 0.020 = 1,000 requests all miss, all decide independently to recompute, and all hit the database with the same query at the same instant. The database was comfortably serving one query per five minutes for that key; it now gets a thousand at once. Nothing was misconfigured. The cache working correctly produced the outage.

naive coalesced 1,000 requests arrive during the 20 ms recompute window 1,000 requests arrive during the 20 ms recompute window singleflight / lock 1,000 queries 1 query the database sees a 1,000× spike on one key 999 callers await the same promise
The fix is not a bigger database. It is recognising that a thousand identical concurrent questions deserve one answer, and that the cache is the natural place to enforce that.
const inflight = new Map();                    // key → Promise, per process

function coalesce(key, loader) {
  const existing = inflight.get(key);
  if (existing) return existing;               // 999 callers join the same promise

  // finally() matters: on rejection the entry must clear, or one
  // transient error is cached as a permanent failure for that key.
  const p = loader().finally(() => inflight.delete(key));
  inflight.set(key, p);
  return p;
}

Be precise about the scope of that fix: it coalesces within one process. With 40 app servers you have gone from 1,000 queries to 40, which is usually enough. If it isn't, the cross-process version is a SET key NX EX 10 lock in Redis — one winner recomputes, the losers either wait briefly or serve the stale value — and you should say out loud that you have just introduced a distributed lock, with the lease expiry and fencing questions that come with it.

TechniqueMechanismCostReach for this when…
Request coalescing / singleflightOne in-flight load per key; everyone else awaits itA few lines; per-process onlyAlways. This is the baseline, not an optimisation
Jittered TTLttl = base ± rand(base × 0.1)One line, no downsideAlways — and specifically whenever many keys are populated together (deploy, warm-up, bulk import)
Early / probabilistic recomputeRefresh before expiry with probability rising as expiry nearsA little duplicate work; needs the last recompute duration storedA handful of extremely hot keys where even one miss is a visible latency spike
Stale-while-revalidateServe the expired value immediately, refresh in the backgroundBounded staleness, by designRead paths that tolerate a few seconds of stale data — which is most read paths
Negative caching + bloom filterCache "does not exist"; or test membership before queryingA short stale window on newly created idsEnumerable keyspaces where a scraper or a bug can request ids that were never real

The probabilistic version is worth naming precisely because it sounds like an invented answer otherwise. XFetch: store how long the last recompute took as delta, and on every read refresh early if now − delta × beta × ln(random()) ≥ expiry, with beta around 1. Expensive-to-compute entries and entries close to expiry refresh sooner, probabilistically, so exactly one unlucky reader usually does the work before anyone has to wait for it (the technique is from Vattani et al., VLDB 2015, linked below).

Vocabulary you will hear, and which failure each word names: penetration is requests for keys that never exist (fix: negative caching, a Bloom filter); breakdown is one hot key expiring (fix: coalescing, refresh-ahead); avalanche is many keys expiring together, or the whole cache tier going away (fix: jittered TTLs, replicas, a gutter pool, load shedding). Two more operational notes: a freshly started or flushed cache is a 0% hit-rate cache, so warm it from replayed traffic or ramp traffic onto it gradually rather than cutting over cold; and a cache tier is a single point of failure unless you have sized the database for its absence.

Hit-rate arithmetic: why 90% is not a good number

This is the section that wins interviews, because almost nobody does the arithmetic. Take a read tier serving 500,000 reads/sec, backed by replicas that each sustain about 20,000 simple indexed point reads/sec. That is this course's conservative planning figure, not a limit: strong hardware does several times more, and OpenAI runs ChatGPT on one Azure PostgreSQL primary plus nearly 50 replicas at millions of aggregate queries per second. Measure your own.

Hit rateMiss rateReads reaching the databaseReplicas required
90%10%500,000 × 0.10 = 50,000/s50,000 ÷ 20,000 = 2.5, so 3
95%5%500,000 × 0.05 = 25,000/s25,000 ÷ 20,000 = 1.25, so 2
99%1%500,000 × 0.01 = 5,000/s1, at 25% utilisation
99.9%0.1%500,000 × 0.001 = 500/s1, at 2.5% utilisation

Going from 90% to 99% is described in conversation as "nine percentage points". In load terms it is a 10× reduction and it takes you from three replicas to one. Going the other way is worse: a hit rate slipping from 99% to 98% doubles database load overnight, and no dashboard labelled "cache hit rate 98%" looks alarming. Always reason in miss rate. Hit rate is a vanity metric; miss rate is the thing that is actually multiplied by your traffic.

The same arithmetic explains the latency shape. With a 0.5 ms cache and a 20 ms database:

  • 99% hit: mean = 0.99 × 0.5 + 0.01 × 20 = 0.695 ms
  • 90% hit: mean = 0.90 × 0.5 + 0.10 × 20 = 2.45 ms — 3.5× worse from a number that "sounds fine"
  • And the percentile that matters: at a 10% miss rate, every request above p90 is a cache miss by definition. Your p95 and p99 are not "the cache is a bit slower sometimes" — they are raw database latency, including its tail. At a 99% hit rate the miss tail begins exactly at p99, so p99 sits on the boundary between cache and database latency; you need roughly a 99.9% hit rate before p99 is a pure cache hit and only p99.9 is the database.
⚠ The failure nobody sizes for: losing a cache node Five Redis nodes, 99% hit rate, 5,000 reads/sec reaching the database. One node dies. Its 20% of the keyspace (this already assumes consistent hashing; with naive modulo hashing nearly every key would move and the miss rate would approach 100%) now misses on every request, so the new miss rate is 0.20 + 0.80 × 0.01 = 0.208, and the database receives 500,000 × 0.208 = 104,000 reads/sec — a 20.8× spike against a tier provisioned for 5,000, and more than five times what one 20,000/s replica serves. The database falls over, the cache cannot be refilled, and the outage is now self-sustaining. This is how cache tiers kill databases. Consistent hashing is what caps the damage at the lost node's share; it does not remove it. What shrinks the spike: replicas of the hot shards, a small gutter pool of spare cache servers that temporarily takes over a failed node's keys with short TTLs (Facebook's memcache design), request coalescing and leases so a thousand misses on one key become one query, and load shedding at the origin. Be honest that your database floor is set by your degraded miss rate, not your healthy one.

Sizing follows the same logic in reverse. Hit rate is a function of how much of the working set fits in memory, and real access distributions are Zipf-ish: often roughly the top 20% of keys serve 80% of requests. That means the first gigabyte buys you an enormous amount and the climb from 90% to 99% can cost several times the memory (workload-dependent; this is an illustrative shape, not a measured constant), because you are now paying to hold the long tail. Knowing where you are on that curve is what makes "should we double the cache?" a calculable question rather than an argument.

Recognizing it in an unseen problem

  • Any prompt with a heavy read:write skew (news feed, product catalogue, URL shortener, profile service) is a caching problem before it is a database problem. State the ratio, then state the miss rate you're targeting and the load that leaves — do not just say "add Redis."
  • A naive design adds a cache and stops there: no TTL policy, no invalidation story, no eviction policy, no stampede protection, and no answer for what happens when a cache node dies. Each of those is a follow-up question the interviewer already has queued.
  • Distinguish caching from replication: a read replica is authoritative and eventually consistent; a cache is non-authoritative and arbitrarily stale. If the prompt needs "must reflect the last write", a replica with read-your-writes routing is the answer, not a cache.
  • Distinguish it from a CDN question: if the payload is large, static and geographically distributed, the win is bandwidth and RTT at the edge, not query offload. Different layer, different invalidation story, same bet.
  • Hot-key language ("a celebrity posts", "a flash sale", "one video goes viral") is the interviewer explicitly asking for stampede handling and hot-key mitigation — coalescing, a per-instance L1 in front of the shared tier, key splitting, or replicating the one hot key across several cache nodes so no single node carries it.
  • Pitfall: caching writes. If the prompt is write-heavy or the data is read once and never again, a cache adds latency and memory cost for nothing. Say "I would not cache this, and here's why" at least once during a loop — knowing where a cache does not belong reads as strongly as knowing where it does.
Before you move on
  • Can you explain why a cache hit must be detected with an explicit miss check (!== null), not by testing the cached value for truthiness, and what breaks when a legitimate 0 or empty string is cached?
  • Can you compute how database load changes when hit rate drops from 99% to 90%, and explain why you should reason in miss rate, not hit rate?
  • Can you name which invalidation pattern — TTL, delete-not-update, versioned keys — fits a given scenario, and why the others don't?
  • Can you say what happens to the database when the cache tier fails, and which of coalescing, a gutter pool, replicas or load shedding you would use to survive it?
  • Can you compute what happens to your database when one node in a 5-node, 99%-hit-rate Redis cluster dies?
  • Can you trace an LRU cache's contents through a short sequence of gets on a small, fixed-capacity cache?
Sources and further reading
  • Scaling Memcache at Facebook (NSDI 2013) — leases against stale sets and stampedes, gutter pools for failed servers, and look-aside caching at very large scale
  • Redis documentation: key eviction — the policies, approximated LRU, and LFU counters with decay
  • RFC 5861: stale-while-revalidate and stale-if-error — how the stale windows are counted from expiry
  • Optimal probabilistic cache stampede prevention (VLDB 2015) — the early-recompute rule described in this chapter
  • Cloudflare: instant purge — reported purge latency, including tag purge
  • OpenAI: scaling PostgreSQL — one primary plus about fifty replicas at millions of queries per second, as a reality check on planning numbers

Claims in this chapter were checked against these sources in October 2026. Figures marked "planning figure" are rules of thumb, not guarantees.

✓Practise

5 questions drawn from 8, scored out of 5. This is practice: it runs in your browser, saves nothing and does not change what you have marked read.

B5

Finished reading?

0 of 36 chapters read in this series

→Next stop

  1. B4Previous · B4Databases in system design
  2. B5you are here
  3. Next · B6APIs & communicationThe API is the coupling surface — every round trip, every version and every retry is a design decision someone will live with for years.22 min read

01On this page

9 sections · 0%

  1. 01Every cache is the same bet, made at a different distance
  2. 02What each layer actually buys you
  3. 03Read strategies: cache-aside vs read-through
  4. 04Write strategies, and when each is right
  5. 05Eviction: what happens when memory runs out
  6. 06Invalidation is hard, and here is exactly why
  7. 07Thundering herd, and the five ways to stop it
  8. 08Hit-rate arithmetic: why 90% is not a good number
  9. 09Recognizing it in an unseen problem

[] chapters · t top