Skip to content
SeeItFlow
System Design · 15 chapters · 28 questions · ~56 min read

Distributed Cache System Design Interview: Questions, Answers & Trade-offs

A distributed cache interview tests whether you treat the cache as a disposable copy with a limited promise, and whether you can say what happens when that copy is stale, missing, overloaded or on the wrong node. This guide gives 28 questions with complete answers, a 45-minute plan, a capacity exercise, one worked sample answer, a rubric and the mistakes that matter. It uses the fictional Meridian Commerce storefront and hypothetical numbers throughout, and the questions are representative practice, not a report of what any company asks.

Mangesh MandlikLast updated: How lessons are made
Chapter 01

What a Cache Interview Is Really Testing

A cache question looks easy at first. Everyone knows to put Redis in front of the database. What the interviewer is listening for comes afterwards: whether you treat the cache as a disposable copy with a limited promise, and whether you can say what happens to the database, the user and the data when that copy is stale, missing, overloaded or on the wrong node.

Most weak answers fail in the same way. They describe the fast path in detail and have nothing to say about the slow one. A strong answer is recognisable by where it spends its time: the refill that races an invalidation, the single product that draws 18% of the reads, the failover that loses a write the cache had already acknowledged, the cold cluster that sends the whole read load back to a database that cannot take it.

This guide holds 28 questions with full answers, a 45-minute plan, a capacity exercise you can redo on paper, one worked answer from start to finish, a rubric, the errors that change a judgment, and some curveballs. The running example is Meridian Commerce, a fictional storefront whose product database is the authority and whose Product API pods read through a cache. Every figure is a hypothetical teaching input or the result of arithmetic on one, and none is a measurement or a claim about a real company. The questions are representative practice material, not a report of what any particular employer asks.

Vocabulary used throughout

TermMeaning in this guide
Cache, source of truthA cache is a faster, temporary copy of data held in memory. The source of truth is the database whose copy wins when the two disagree.
Redis, MemcachedTwo widely used in-memory key-value stores. This guide uses them as reference points and says when a behavior belongs to one of them rather than to caches in general.
Hit, miss, hit rateA hit is a lookup the cache answers; a miss is one it cannot, so the request goes to the database. Hit rate is hits divided by lookups.
Cache-asideThe application checks the cache, reads the database on a miss, and writes the result back into the cache itself.
TTL, expiry, evictionTTL (time to live) is the lifetime given to an entry. Expiry removes entries whose TTL ended. Eviction removes entries early because memory is full.
Pod, Product APIA pod is one running copy of the application server. The Product API is the service that reads product data from the cache and database.
Shard, primary, replicaA shard is one slice of the cached data. Each shard has a primary node that takes writes and may have a replica node that keeps a copy so it can take over.
FailoverPromoting a replica to primary when the primary is lost.
Hash slotOne of 16,384 fixed buckets that Redis Cluster maps keys into; each slot belongs to exactly one primary.
Single-flightLetting only one caller load a missing key while the others wait for that result.
L1 cacheA very small cache inside each pod, in front of the shared cache.
Conditional fill, record versionA cache write that succeeds only if the entry is not newer than the version the writer read from the database.
TombstoneA short-lived marker left in place of a deleted entry, carrying the version that deleted it.
Stale-while-revalidate, stale-if-errorServing an expired value while it is refreshed in the background, or when the refresh fails.
Admission controlA cap on how many requests may reach the database at once; the rest are rejected or degraded.
p95, p99The latency below which 95% or 99% of requests complete; they expose slow tails that a mean hides.
Each term is defined again where an answer first needs it; this table is for quick reference.
Chapter 02

A 45-Minute Plan, Scope Questions and Whiteboard Order

Forty-five minutes disappears quickly. The plan below keeps the first ten minutes for scope and numbers, because every later choice (local or shared, how many shards, which TTL) falls out of them.

MinutesPhaseWhat you doOn the board afterwards
0–5ScopeAsk what is cached, who reads it, how wrong it may be and for how long, and what happens if the cache disappears. Name the non-goals: the cache will not hold inventory reservations, payment data or permission decisions.Four or five requirement lines and the data classes (descriptive, price, availability, never-cached), which the answers below call C1 to C4.
5–12NumbersAverage 12,000 req/s, peak 60,000, 80% cache-eligible, 90% hit rate, 1.5 KiB objects, 8,000,000 hot objects. Derive database load and memory. Label every assumption.Peak database reads 16,800/s against a hypothetical 24,000/s capacity; 15.27 GiB of cache memory.
12–22Core pathCache-aside read, write with invalidation, TTL with jitter, one eviction policy. Walk one read and one price change.API pods, shared cache, database, an invalidation stream, and the refill race with its remedy.
22–34ScaleWhy one node is not enough, how keys find a shard, how the cluster grows, what a hot key does, what replicas do and do not promise.Four shards with a slot map, async replicas, a hot-key relief box.
34–42FailureCache outage at peak, failover loss, tenant leak, regional staleness. Say what is bounded and what is shed.Failure table with a detection signal and a response per row.
42–45CloseRestate the contract: what is guaranteed, what is not, and how checkout revalidates.A three-line summary.
Time boxes are a suggestion. If the interviewer pulls you into one area, follow them and cut the others.

Scope questions worth asking out loud

  • Which objects are cached, and which of them can be wrong for a few minutes without harming anyone (descriptions) versus not at all (inventory reservations, permissions)?
  • What is the read-to-write ratio per object class, and how skewed are the reads? One product with a fifth of the traffic changes the design.
  • What is the database's real read capacity, and what does it do when overloaded: queue, slow down, or fail?
  • Is the cache allowed to be down? If so, who absorbs the load, and which request classes get degraded service?
  • Is data shared across tenants or regions, and are there residency or erasure obligations?
  • Which numbers are given and which should I assume? State the assumptions rather than hiding them.

Whiteboard order

Draw the database first and say it is the authority. Add the Product API pods, then the shared cache beside them with a read arrow that falls through to the database on a miss; each box should answer a weakness of the previous drawing. After the read path comes the write path and its invalidation stream, then memory and expiry, then sharding with replicas, and only then regions and tenant boundaries. Adding boxes in that order lets the interviewer interrupt at any point and still see a coherent system.

Swipe to pan
Cache-aside: hit path and miss pathCache-Aside: The App Owns the CacheRead path for product:4821 · the cache never talks to the DB by itselfShopperProduct APICacheProduct DBHIT path ≈ 3 ms1 GET /products/48212 GET v2:cedar:product:48213 hit: $99 · v5MISS path ≈ 85 ms (illustrative)4 GET → miss5 SELECT product 48216 row: $99 · v57 SET value, TTL 300 s ± 20%8 200 OK · $99A failed cache write at step 7 must not fail the request: the shopper already has the data.Weakness: steps 5–7 are not atomic with writers (stale-fill race); every miss costs the full path.Variants: read-through (the cache loads for you), write-through, write-behind (risk of dirty data).
The first path to draw: a read checks the cache, falls through to the database on a miss, and fills the cache on the way back. Everything else in the interview modifies this picture.
Chapter 03

Capacity Exercise With the Canonical Numbers

Capacity questions are a test of discipline more than arithmetic. State the assumption, do the multiplication where the interviewer can see it, and say what the result does and does not prove. Every input below is illustrative.

QuantityWorkingResultAssumption to state aloud
Peak eligible reads60,000 × 0.8048,000/s80% of requests are reusable product reads.
Peak hits and misses48,000 × 0.90 and × 0.1043,200 hits, 4,800 misses /sA 90% hit rate on eligible reads only.
Non-eligible requests60,000 × 0.2012,000/sThese always reach the database; assume nothing else does.
Database load at peak4,800 + 12,00016,800/sAgainst a hypothetical 24,000 reads/s capacity this is 70%; the planned ceiling at 80% is 19,200/s.
Without a cache60,000 vs 24,0002.5× capacityOverload: queues grow, latency rises, errors follow.
Raw payload8,000,000 × 1.5 KiB11.44 GiB (12.288 GB)Payload only. GiB are powers of two, GB powers of ten.
Memory per entry (assumed)1,536 + 40 key + 64 metadata1,640 BOverheads are assumptions; measure with the engine's own tooling.
Cluster memory1,640 B × 8M = 12.22 GiB, × 1.2515.27 GiB1.25 allocator/fragmentation factor, assumed.
Per shard (4 shards)15.27 ÷ 43.82 GiB mean; 4.39 GiB hottest at ×1.1554.9% of an 8 GiB budget for the hottest shard.
With one replica each15.27 × 230.5 GiB over 8 nodesReplicas hold a complete copy.
Cache operations at peak48,000 GETs ÷ 412,000/s per shardAgainst a hypothetical 25,000 ops/s budget: 48%.
Every figure is derived from teaching inputs, not measured.
Swipe to pan
Cache capacity worksheetThe Capacity Worksheet: Every Number From One Chain of Arithmeticall inputs are teaching assumptions, not measurements; measure with your engine's own toolsTraffic (peak)60,000 req/s × 80% eligible= 48,000 eligible reads/s× 90% hit = 43,200 hits/s4,800 misses + 12,000 not eligibleaverage: 12,000 → 3,360 to the DBDatabase load (peak)4,800 + 12,000 = 16,800 reads/s28% of the 60,000 no-cache load= 72% fewer database readsvs 24,000 capacity: 70% busyno cache: 2.5× capacity (overload)Memory (assumed overheads)8M × 1.5 KiB = 11.44 GiB payload× 1,640 B per entry = 12.22 GiB× 1.25 fragmentation = 15.27 GiB÷ 4 shards = 3.82 GiB (hottest 4.39)4.39 = 54.9% of 8 GiB; +replicas 30.5Shard operations (peak)48,000 GETs/s ÷ 4 = 12,000 per shardbudget 25,000/s → 48% busyhits and misses both start with a GETthe budget is a teaching input,not a benchmarkHot key (18% of eligible)8,640 req/s on one keyhot shard 18,480/s = 73.9% of 25,000other shards 9,840/s (1.88× skew)L1: 2 s TTL × 20 pods→ at most 10 shard GETs/s for the keyCache outage at peak48,000 eligible reads/s hit the backendcap: 19,200 − 12,000 = 7,200/sshed / stale / degrade: 40,800/s (85.0%)48,000 / 7,200 = 6.7× oversubscribed2,880,000 requests per minuteLatency of cache-eligible reads (illustrative: hit 3 ms, miss 85 ms)mean = 0.9 × 3 + 0.1 × 85 = 2.7 + 8.5 = 11.2 ms · p95 and p99 sit at about the miss latency (~85 ms), far above the mean.Payload-only 11.44 GiB is not the memory bill: key, metadata, fragmentation, replicas and headroom must be counted.
The worksheet on one page: load, hit rate, memory with its assumptions, and per-shard budget.

Two things make this exercise convincing. The first is saying what it leaves out: the 90% hit rate is a guess that real traffic will confirm or refute, the 16,800 reads per second assume nothing else touches the database, and the memory model has three assumed overheads that a production team would replace with measured ones. The second is moving on to the question that actually decides the design: with these numbers the cache absorbs 72% of peak database load (1 − 16,800 ÷ 60,000), which means that if the cache vanishes at peak the database sees 48,000 additional requests per second, not 4,800.

Chapter 04

One Complete Sample Answer

Prompt: "Design a cache for the product pages of a storefront that has traffic peaks and a relational product database." What follows is how an answer can run in about twelve minutes of speaking, written out so the reasoning is visible.

I would begin by pinning down what is being cached and how wrong it may be. Product descriptions change rarely, so a stale one for fifteen minutes is harmless. The price on a product card matters more, but it is display only, because checkout reads price and stock from the authoritative service anyway. An availability badge is informational and can be thirty seconds old. Reservations, payment instruments and permission decisions are a different class entirely: they never come from a TTL-only cache. That classification is the design; the rest is mechanism.

For load, the average is 12,000 requests per second and the peak is 60,000, with 80% of them cacheable reads. At peak that is 48,000 eligible reads. At a 90% hit rate the database sees 4,800 of those plus the 12,000 that were never eligible: 16,800 per second, which is 70% of an assumed 24,000 capacity and leaves room under a planned ceiling of 19,200. Without the cache it would be 60,000, two and a half times capacity, so the cache is not an optimization here; the database cannot serve the peak without it. I would say that and then say I am assuming the hit rate and will validate it.

The read path is cache-aside. The Product API asks the cache for v2:cedar:product:4821; on a hit it returns, on a miss it reads the database, fills the cache with the value and its record version, and returns. The key carries a schema version and the tenant, so an application deploy that changes the serialization cannot read an incompatible value, and one tenant cannot construct another's key by accident. For writes, the database commits first, then a transactional outbox (an event row written in the same database transaction as the change and published afterwards, so the event cannot be lost if the process dies) emits an event, and a subscriber deletes the key. I would not update the cache from the writer, because two writers can interleave and leave the older value last.

I would raise the race before the interviewer does. A reader misses and reads the old row, the writer commits and deletes the key (which does nothing, since the key is empty), and then the reader's slow fill lands the old price. The remedy is a conditional fill that compares record versions, or, if that is too heavy, accepting bounded staleness through a 300-second TTL with ±20% jitter, which caps the worst case at about 360 seconds. Jitter matters because 6,000 keys warmed together would otherwise expire together; spread over 120 seconds they expire at about 50 keys per second.

Then scale. One shared node is a single failure and load domain, so I would shard. With four shards, hash slots (a fixed set of 16,384 buckets, each key mapped to one by a CRC16 checksum) beat modulo hashing because adding a shard moves only the slots I choose to hand over, here 4,096 of them, a quarter of the data, instead of nearly everything. Each primary gets an asynchronous replica for availability, and I would be explicit that a failover can lose a write the primary had already acknowledged. That is acceptable only because the database is the authority and TTL plus invalidation will repair the entry.

Last, failure. The hot product at 18% of eligible reads is 8,640 requests per second on one key, which puts one shard at 18,480 operations per second while the others sit at 9,840, so I would add a two-second per-pod local cache for that key and accept two seconds of price staleness on it. If the whole cache is down, the database can admit only 7,200 more requests per second after non-eligible traffic, so 85% of eligible reads must be served stale where permitted, degraded or shed. The cache is a performance layer whose absence has to be survivable, and I would close by saying exactly that.

Chapter 05

Fundamentals: Justifying a Cache and Its Read Paths

Four questions that set the vocabulary: when a cache is justified, what the read patterns mean, why a per-process cache is not enough, and what a miss actually does.

Q

When is a cache justified rather than improving the database query?

Strong answer angle

Fix the query first when the query is the problem, and add a cache when the query is already reasonable but the same answer is requested far more often than it changes. A missing index, an N+1 access pattern or a table scan will make the database slow for every caller, and a cache only hides that on the keys that happen to be warm; the first cold or evicted key still pays the full cost. Look at the numbers before choosing. If a product read costs 3 ms from an in-memory cache and 85 ms from the database (illustrative), and 80% of 60,000 requests per second are repeats of reusable reads, then even a perfect index leaves the database serving the full 60,000 per second, 2.5 times a hypothetical capacity of 24,000. Better indexing makes each read cheaper, but it does not remove the reads. A cache does remove them: at a 90% hit rate on eligible reads the database sees 16,800 per second. A cache is justified when the read volume is high, the data tolerates a defined amount of staleness, there is meaningful repetition (skewed popularity helps), and the system can survive the cache being empty. It is a poor choice when data is written as often as it is read, when every read must be fresh, when keys are almost never repeated (a low hit rate pays the lookup cost for nothing), or when the database could simply be scaled with replicas at lower operational risk. I would also note that a cache adds a failure mode: the system can come to depend on it, so its absence at peak must be planned for.

Likely follow-ups
  • What hit rate makes a cache worthwhile? — There is no universal threshold. Compare the cost of a lookup plus the miss path against the saving: at 3 ms for a hit and 85 ms for a miss, a 10% hit rate would save little, while 90% cuts mean eligible latency to 11.2 ms. The more important question is whether the database survives without it.
  • Would read replicas be enough? — Sometimes. They scale reads with the same consistency model as the database and no invalidation logic, but they cost a full database each and cannot match in-memory latency. Choose them first if load is the only issue.
Q

What is the difference between cache-aside, read-through and write-through?

Strong answer angle

The difference is who talks to the database and when. In cache-aside, also called lazy loading, the application owns the logic: it reads the cache, on a miss reads the database itself, then writes the value into the cache. Only requested data gets cached, the cache can be empty or down without breaking correctness, and the application can decide what to cache and for how long. The cost is that every caller must implement it correctly, and the first request for a cold key is slow. In read-through the cache sits in front of the database and loads on a miss through a loader it was given; the calling code only asks the cache. That centralises the logic and makes request coalescing easy to put in one place, but it needs a cache layer that can reach the database and enforces one way of loading. Write-through is about writes: the application writes to the cache and the cache synchronously writes to the database before acknowledging, so cache and database agree after each write, at the price of write latency and a cache that now sits in the critical write path. A fourth variant, write-behind, acknowledges after writing the cache and persists later; it is faster, but a cache crash loses writes the user was told succeeded, so it makes cache failures a business error instead of a performance problem. For Meridian's product reads I would pick cache-aside with invalidation, because the cache is allowed to be wrong or empty and the database must stay the single writer. I would choose read-through if many services shared the cache and I wanted one implementation of loading and coalescing, and write-through only for data that is read immediately after it is written and where the cache is not the sole copy.

Likely follow-ups
  • Does write-through remove stale data? — It removes one cause, because the cache and database are written together for that path. Other writers that go directly to the database, replicas lagging, or a failed second write still leave inconsistencies, so a TTL is still useful as a backstop.
  • Where does write-behind belong? — Only where losing the most recent writes is acceptable and measured, such as counters or analytics, and never for the system of record.
Q

Why is a process-local cache not enough behind a load balancer?

Strong answer angle

Because each pod holds its own private copy, so you get three problems at once: duplicate memory, uneven freshness and a lower hit rate. Take 20 Product API pods and a request for product:4821. The load balancer spreads that product's traffic across all 20, so the key has to be fetched and stored 20 times, once per pod, and each pod misses independently after every restart or expiry. Memory is multiplied by 20 for the same data. Worse, when the price changes from $99 to $109, one pod can refresh while another keeps showing $99 until its TTL ends, so two shoppers hitting different pods in the same second see different prices; in-process caches also cannot easily be invalidated from outside without a broadcast channel. A shared cache removes duplication and gives one place to invalidate: after the first miss every pod hits. That is not a reason to throw local caches away. A tiny local cache (L1) with a two-second TTL in front of the shared cache is useful for a single very hot key, because it removes almost all of its load from one shard: 20 pods fetching at most once every two seconds is at most 10 shard reads per second for that key. The cost is up to two seconds of staleness on those entries and an extra invalidation path. Use L1 deliberately for a few hot keys, not as the main cache.

Likely follow-ups
  • When is local-only enough? — For small, rarely changing reference data (configuration, feature definitions) where a few seconds of divergence is harmless and the data set is tiny.
  • How would you invalidate L1? — Subscribe each pod to the invalidation stream and delete the key locally; keep a short TTL as the backstop, since a pod can miss an event.
Q

How does a cache miss flow from the API to the database and back?

Strong answer angle

Start with a shopper requesting product 4821 from tenant Cedar. The Product API builds the key v2:cedar:product:4821 and sends a GET to the shard that owns it. The shard answers with nothing: that is the miss. The API then reads the row from the product database, which takes about 85 ms in the illustration compared with 3 ms for a hit. The database returns the row with its record version, for example v5 and a $99 price. The API serialises it into the value envelope (schema version, record version, creation and expiry times, compression flag, bytes), checks it is under the 1 MiB size cap, and sends a SET with a TTL, which for a price is 300 s with random jitter of ±20%. The reply to the shopper does not need to wait for the fill; some implementations send the response and fill asynchronously, though then a failed fill is silently lost, which is fine for a cache. Several details are where it goes wrong. If the database read fails, the API must not cache an error. If 5,000 requests miss the same key at once, they all go to the database unless the API coalesces them. If the SET times out, the outcome is unknown: the value may or may not be there, which is harmless for a plain fill but matters for conditional or counter operations. And if the product genuinely does not exist, store a negative entry for 30 s so repeated lookups do not all reach the database.

Likely follow-ups
  • Should the API wait for the fill? — Not usually. Return the value and treat the fill as best effort, with a short timeout and a metric for failures, so a slow cache never slows the shopper beyond the miss itself.
  • What if the database is down on a miss? — Serve stale only for classes where policy allows it (and the entry still exists), otherwise return an error; never fabricate a value or cache the failure.
Chapter 06

Stampedes, TTLs, Races and Stale Reads

The questions here are about time: what a timer guarantees, what concurrency does to a fill, and how much wrongness each class of data can bear.

Q

What is a cache stampede, and how does single-flight help?

Strong answer angle

Take product 4821 at 8,640 requests per second with an 85 ms miss path. When its entry expires, every request that arrives before the first refill completes is a miss. That is 8,640 × 0.085 ≈ 734 requests all asking the database for the same row; in a flash sale it can be 5,000 concurrent misses on one expired key. The database does the same work 734 or 5,000 times and may slow down, which lengthens the miss path, which lets more requests pile up: the stampede feeds itself. Single-flight means that for a given key only one caller performs the load and everyone else waits for its result. Inside one process this is a map from key to an in-flight future; the first caller creates it, later callers await it. With 20 pods that cuts 5,000 database fetches to at most 20, about 250 waiters per pod. To get to one, use a distributed lease: a caller sets a short-lived lock key in the cache, and only the holder loads; others wait briefly, serve a slightly stale value, or retry. The details matter. A lease needs an expiry so a crashed holder does not block the key forever, and a lease can expire while the holder is still working, so the fill itself should be conditional. Waiters need deadlines and a fallback, or the lock simply moves the queue. Single-flight does not help when many different keys expire together: lock-per-key does nothing for 6,000 keys that all expire at one moment. That case needs TTL jitter (spread over 120 s, about 50 expirations per second), stale-while-revalidate, or refreshing before expiry.

Likely follow-ups
  • Is per-process single-flight enough? — For a small fleet it is often enough: 20 fetches instead of 5,000 is a different order of load. Add a distributed lease only when the remaining multiplication still hurts.
  • What about probabilistic early refresh? — It lets some requests refresh slightly before expiry with a probability that rises as expiry approaches, which avoids a synchronized cliff without a lock. It comes from published work on optimal probabilistic early expiration (often called XFetch); tune it against your own traffic.
Q

What does a TTL guarantee, and what does it not?

Strong answer angle

A TTL gives an upper bound on how long an entry may be returned, nothing more. For Redis, a key with an expiry is never returned after it has expired, and the server removes it by two routes: when someone touches it, and by periodically sampling keys that have an expiry, so an expired key can sit in memory for a short while without ever being served. Memcached documents the same promise for a non-zero expiry: it will not be retrievable afterwards, though its protocol notes that expiry is only accurate to about a second and that a TTL of 1 can expire at once. That is the whole guarantee. A TTL does not mean that the value is fresh before the deadline. If the price changes at second 10 of a 300 s entry, readers may see the old price for 290 more seconds, and nothing synchronises the two events. It does not mean that all caches agree, since replicas and local caches have their own timers. It does not stop the entry being evicted early: under memory pressure a key may disappear long before it expires, and in Memcached an expiry of zero means never expires but may still be deleted to make room. And a TTL does not protect against a stampede; it causes one when many entries share a deadline. In design terms TTL is a backstop for lost invalidations plus a staleness budget: for Meridian's price, event invalidation brings freshness within seconds normally, and a 300 s TTL with ±20% jitter means the worst case after a lost event is about 360 s.

Likely follow-ups
  • Should the TTL be shorter for volatile data? — Yes, within reason; TTL is a staleness budget per class. Thirty seconds for an availability badge, 300 s for a price with events, 15 minutes for descriptions. Very short TTLs raise the miss rate and database load.
  • Do replicas expire keys themselves? — In Redis, replicas do not delete expired keys themselves: the primary expires the key and sends a DEL. A replica does, however, use its own clock to hide a logically expired key from reads while it waits for that DEL, and once promoted it expires keys independently (Redis replication documentation).
Q

How can an invalidation race leave an older value in the cache?

Strong answer angle

Lay it out as four steps with no locks involved. At t0 a reader on pod A misses on product:4821 and reads the database: price $99, version v5. The read is slow, maybe a garbage-collection pause or a busy connection. At t1 an editor changes the price to $109 (v6) and the commit succeeds. At t2 the invalidation subscriber deletes the cache key, which is already absent, so the delete does nothing useful. At t3 pod A finally finishes and fills the cache with $99, v5. The cache now holds the old price and no further event will correct it, because the invalidation already happened. Until the TTL ends, every reader sees $99. Deleting is not wrong; the problem is that the fill is an unconditional write based on a read taken before the commit. Remedies come in strengths. First, accept it: with a 300 s TTL ±20% the staleness is bounded at about 360 s, and checkout revalidates, which is acceptable for a display price. Second, make the fill conditional: store the record version in the entry, and have the fill succeed only if no newer version is present. That needs the atomic compare to run in one place (a small server-side script in Redis, or compare-and-set tokens in Memcached), and it needs something for the delete to leave behind, either a short-lived tombstone carrying v6 or a write of the new value, because an empty key carries no version to compare with. The version must come from the authority, the database's row version, not from a cache clock or an application timestamp. Third, delay a second delete a short time after the write, which narrows the window but does not close it.

Likely follow-ups
  • Why not update the cache on write instead of deleting? — Two concurrent writers can update the cache in the opposite order from their commits, leaving the older value last; updates also require writers to know every cached representation. Delete plus versioned fill is easier to reason about.
  • Does a tombstone cost anything? — It occupies a key for a short TTL and adds a read-path branch. It buys a version to compare against; keep the TTL just longer than the longest plausible database read.
Q

How do you mitigate stale price and inventory reads?

Strong answer angle

Separate what is displayed from what is decided, and treat each class differently. The product card price is display only; invalidation by an outbox event plus a 300 s TTL with jitter keeps it within seconds in normal operation and at most about 360 s if an event is lost, and checkout re-reads the price from the authoritative service, so a stale $99 on screen does not charge $99. The availability badge is a 30 s TTL entry for display. A cache entry for stock must never reserve or promise anything: the reservation happens against the inventory service and either succeeds or fails there. So the user may see In stock and then be told the item sold out at checkout, a UI cost rather than an oversell. If that experience is unacceptable for a particular launch, shorten the TTL or bypass the cache for the last step, at the price of more database traffic. For authorization the rule is stricter: permission decisions are never served from a TTL-only cache, since a revoked permission must fail safe. Check the authority, or compare a versioned revocation epoch with the entry. Stale-while-revalidate (serve the old value for up to 5 minutes while one request refreshes) is a fine choice for descriptions and a poor one for permissions or ownership of stock. Monitoring completes the picture: log the age of served entries and the rate of checkout price corrections, since a rising correction rate means the staleness budget is too wide.

Likely follow-ups
  • Can you make the badge exact? — Not from a cache. The best you can do is a short TTL plus event invalidation; exactness belongs to the inventory service at reservation.
  • How would you measure staleness? — Compare record version and age in served envelopes with the database version on a sampled basis, and track checkout corrections.
Chapter 07

Memory, Eviction and Measuring a Cache

Questions about a finite resource and about numbers that are easy to misstate: which entries to throw away, how engines manage memory, how to size a cluster, and what the hit rate really means.

Q

How would you choose an eviction policy for skewed reads?

Strong answer angle

Begin from the access pattern, since each policy encodes a guess about it. Eviction is what a cache does when memory is full; expiry is separate, and removes entries whose time has run out. LRU (least recently used) evicts the entry untouched for the longest time and suits traffic where something read recently will probably be read again. LFU (least frequently used) evicts the one used least often and suits skewed popularity, where a few products draw most reads and must not be pushed out by a one-off burst. Random is cheap and reasonable when accesses are roughly uniform. A TTL-first policy evicts whatever is closest to expiring, which only works if entries carry expiries. For Meridian the reads are skewed: the top product alone is 18% of eligible peak traffic. That points to LFU or an LRU variant, with a caveat about scans. Suppose a crawler requests 2 million distinct products once each. Under plain LRU it can push out the hot working set, because each crawled entry is the most recently used for a moment; LFU resists that, since a single touch earns a low frequency. LFU has its own weakness: an old favourite can stay long after interest fades, so engines add decay. In Redis, LRU and LFU are approximations; LRU samples a few keys (the docs mention maxmemory-samples, default 5) rather than keeping an exact list, and LFU uses a small probabilistic counter with decay. The docs describe allkeys-lru as a good default when there is no reason to prefer another, and volatile-* policies only consider keys with an expiry, behaving like noeviction if there are none. I would measure rather than guess: replay a sampled access log against candidate policies and compare hit rate, and watch the eviction rate in production, because evicting entries that are still hot shows up as a falling hit rate and more database load.

Likely follow-ups
  • What if memory fills with entries that never expire and noeviction is set? — Writes fail with an error. For a cache that is usually wrong: choose an allkeys policy, or make sure everything carries a TTL.
  • Should you tune eviction for the hot key? — Probably not; a very hot key survives any decent policy. Tune for the long tail that determines the overall hit rate.
Q

How do Memcached's slabs differ conceptually from Redis memory management?

Strong answer angle

Think of it as pre-sorted shelves versus individually allocated items. Memcached divides memory into pages and gives each page to a slab class, a class of fixed-size chunks (the stats output reports a slab class id). An item goes into the smallest chunk size that fits, so some bytes in each chunk are wasted, which is internal fragmentation, and each class has its own LRU. The consequence is that if the size mix changes (a deploy makes values larger, say), pages are sitting in classes that are no longer useful while the new class has too few; Memcached has slab rebalancing for that (slabs reassign and slabs automove appear in the protocol documentation, which notes they may change), and an LRU crawler to reclaim expired items. Redis, by comparison, typically stores each key and value as separately allocated objects through a general-purpose allocator and enforces a total maxmemory limit with a global eviction policy across keys, so there is no per-size-class eviction. Its fragmentation shows as resident memory exceeding the bytes the dataset logically uses, and persistence or replication adds buffers and copy-on-write overhead, which is why the docs advise leaving headroom for them (INFO memory reports an amount not counted for eviction). The two also differ in data model: Memcached offers simple key-to-bytes items with cas tokens, and distribution is usually done by the client hashing keys across servers rather than built into the server; Redis adds structures, replication and a cluster mode. Details such as allocator choice, default item size (about 1 MiB by default and configurable) and automove behavior vary by version, so I would check the documentation for the version in use rather than quoting defaults.

Likely follow-ups
  • Which would you pick for a plain product cache? — Either can work. Choose Memcached-like simplicity when you need only key-value reads with client-side sharding, and Redis-like features when you want structures, replicas or server-side scripts. Operational familiarity often decides it.
  • Why does value size matter in a slab design? — Chunk sizes round up, so many values just over a boundary waste memory; check the size histogram before setting growth parameters.
Q

How would you size memory, and account for fragmentation and replicas?

Strong answer angle

Payload arithmetic gives the floor and is almost never the answer. 8,000,000 objects × 1.5 KiB is 11.44 GiB (12.288 GB in decimal units; say which). Now add what the engine stores around each object. I would assume, and label as assumed, a 40-byte key and 64 bytes of metadata, hash-table and pointer overhead per entry: 1,536 + 40 + 64 = 1,640 bytes, so 8,000,000 entries come to 12.22 GiB. Allocators and slab classes waste memory too; with an assumed 1.25 fragmentation factor the total becomes 15.27 GiB. Spread across four shards, that is 3.82 GiB each on average, and if the busiest shard holds 15% more than the mean (an assumption about uneven distribution), 4.39 GiB, which is 54.9% of an 8 GiB budget. The nominal 4 × 8 = 32 GiB cluster is 47.7% utilised on average. One replica per primary doubles the bill: about 30.5 GiB across 8 nodes. Then reserve more. Replication and client buffers, and snapshot or fork behavior if persistence is on, need headroom the eviction logic does not count; I would not plan to run above something like 70–75% of the budget (a judgment, not a rule). Every overhead above is a placeholder: load a realistic sample into the actual engine and read its own memory report to replace them. It is equally important to size for the working set rather than the whole catalog: if 8,000,000 objects are hot but the long tail is cold, a smaller cache with a lower hit rate may be a better trade.

Likely follow-ups
  • What if hit rate falls when you shrink the cache? — That is the experiment: measure hit rate against cache size on real traffic and pick the knee of the curve.
  • Does compression help? — It trades CPU and latency for memory. It pays for large compressible values and hurts tiny ones; store a compression flag in the envelope so mixed versions can coexist.
Q

How do you calculate hit rate, miss rate and real backend request reduction?

Strong answer angle

The three are different quantities and mixing them is the commonest accounting error. Hit rate is hits divided by cache lookups: of 48,000 eligible GETs per second at peak, 43,200 are hits, so 90%, and the miss rate is the remaining 10% (4,800). Backend request reduction compares database reads with and without the cache, across all traffic. Without a cache the database serves all 60,000 requests per second. With it, the database serves the 4,800 misses and the 12,000 non-eligible requests: 16,800. Reduction is 1 − 16,800 ÷ 60,000 = 72%, not 90%, because 20% of traffic never benefits. At the average load it is also 72% (3,360 against 12,000). Two further distinctions: latency and tails. With hits at 3 ms and misses at 85 ms (illustrative), the mean for eligible requests is 0.9 × 3 + 0.1 × 85 = 11.2 ms, but at a 10% miss rate the 95th and 99th percentiles are about the miss latency, 85 ms. A dashboard that shows only the average hides that. A reasonable dashboard computes hit rate per key class, per tenant and per shard (an overall figure can hide one collapsing class), counts a negative-cache hit separately, and excludes bypass or error paths from the denominator, saying how.

Likely follow-ups
  • Why report per class? — A healthy 90% overall can hide a price class at 40% hit rate that is doing the damage.
  • Is a falling hit rate always bad? — Not always: a deploy that adds new key variants or a flash event of cold keys lowers it. It becomes an incident when database load approaches its ceiling.
Chapter 08

Sharding, Routing and Migration

One cache node is a single failure and capacity domain, so the data must be split. The questions below are about how keys find their shard and what it costs to change the number of shards.

Q

Why does modulo hashing reshard so many keys when adding a node?

Strong answer angle

Because the owner is hash(key) mod N, and changing N changes the answer for almost every key. Go from 3 nodes to 4: a key stays only when hash mod 3 equals hash mod 4. For a large uniform sample the fraction that changes owner is N ÷ (N+1), so 3 ÷ 4 = 75%. With ten nodes becoming eleven, 90.9% move. A tiny sample shows it: use illustrative hash values 17, 22, 35, 41, 58, 64, 70, 83, 96, 101, 112 and 120 (not a real hash function). Ten of the twelve land on a different node when N goes from 3 to 4, 83% in this small sample against the 75% expected. For a cache the movement is not a data migration problem but a cold-cache problem: those keys are not at their new owner, so reads miss, the misses go to the database, and the hit rate collapses just when you added capacity because load was already high. With 4,800 misses per second at peak and 75% of the keyspace suddenly cold, the database would face far more than its 19,200 per second planned ceiling. Modulo is fine for a fixed number of nodes that never changes, and unsuitable when the cluster must grow or lose nodes. The fix is a scheme in which a change relocates only a share of keys.

Likely follow-ups
  • Could you warm the new node before switching? — Yes, by replaying hot keys or shadow-reading, but the placement function still moves most keys, so you would warm most of the cache.
  • What about removing a node? — The same: the remainder of the keyspace is rehashed unless you use a ring or slots.
Swipe to pan
Modulo hashing remaps most keys when a node is addedModulo Placement: Adding One Node Remaps Most Keysnode = hash mod N · 12 illustrative key hashes, not a real hash functionkey hashmod 3 (A–C)mod 4 (A–D)17CB22BC35CD41CB58BC64BA70BC83CD96AA101CB112BA120AA10 of 12 keys change owner (red) — a small sample; the expected share is 3/4 = 75%Each change is a cache miss on a cold node, so the database sees a wave of misses right after a routine resize.Share of keys that change owner when one node is added3 → 4 nodesmodulo 75.0%balanced ring or slots ≈ 25.0% (1 / (N+1))4 → 5 nodesmodulo 80.0%balanced ring or slots ≈ 20.0% (1 / (N+1))10 → 11 nodesmodulo 90.9%balanced ring or slots ≈ 9.1% (1 / (N+1))
Moving from three to four nodes under modulo placement relocates roughly three quarters of the keys.
Q

Explain consistent hashing and virtual nodes with a tiny example.

Strong answer angle

Picture a circle of hash positions 0 to 99 (an illustrative space). Each node owns tokens on the circle, and a key belongs to the first token clockwise from its hash, at or after it. Let A hold tokens 10 and 60, B hold 25 and 80, and C hold 40 and 95; each token is a virtual node, one of several positions a physical node occupies. Ownership over the 100 positions works out to A 35, B 35, C 30. The key product:4821 hashes to 33; the next token clockwise is 40, so it lives on C. Now add node D with tokens 20 and 70. D takes the ranges (10, 20] and (60, 70], both of which previously belonged to B. That is 20 of 100 positions, and they all move from B to D; no key moves between A and C, and key 33 stays on C. Compared with modulo hashing, which would move about three quarters of the keys for a 3 to 4 change, the movement is limited to what the new node takes over. Notice also that the result is uneven (A 35, B 15, C 30, D 20) because we gave each node only two tokens; real systems place many virtual nodes per physical node so that shares average out and so that a removed node's load spreads over many survivors instead of dumping onto one neighbor. For balanced rings the expected movement when going from N to N+1 nodes is roughly 1/(N+1) of keys; I would not quote more specific percentages for a real ring without measuring it.

Likely follow-ups
  • What does a virtual node buy over a single token? — Smoother distribution and finer-grained rebalancing, at the cost of a larger routing table.
  • Does the ring fix a hot key? — No. A single key has one hash and one owner however the ring is cut.
Swipe to pan
Consistent hash ring: adding a node moves only its own rangesConsistent Hash Ring: A New Node Takes Only Its Own Rangesillustrative hash space 0–99 · a key belongs to the first token clockwise (at or after its hash)A10D20B25C40A60D70B80C95key 33→ owner C (token 40)red outline = ranges moved to DPositions owned (of 100)Before: A{10,60} B{25,80} C{40,95}A 35B 35C 30After adding D{20,70}A 35B 15C 30D 20What movedD takes (10,20] and (60,70], both B's20 of 100 positions (20%) move, all from BA, C and key 33 (still C) are untouchedCaution: unevenB now owns 15, D 20: two tokens per node istoo coarse. Real rings give each node manyvirtual nodes to even this out.A balanced ring moves about 1/(N+1) of keys;modulo 3 → 4 moved 75% (modulo diagram).
Adding node D claims two ranges from B and leaves every other key where it was.
Q

Why isn't Redis Cluster's fixed-slot mapping the same algorithm as a generic consistent-hash ring?

Strong answer angle

Redis Cluster does not place keys on a ring. It defines 16,384 hash slots and computes HASH_SLOT = CRC16(key) mod 16384 (a hash tag inside braces, when present and non-empty, is the only part hashed). Each primary serves a set of slots, kept in an explicit slot-to-node table, and resharding moves whole slots on purpose, using commands that put a slot into migrating and importing states. In my three-primary illustration A has 0–5460, B 5461–10922 and C 10923–16383; product:4821 hashes to slot 13107 and so lives on C, while tenant:cedar:product:4821 is slot 8298, on B. To add D, an operator or planner hands over 4,096 slots, a quarter (say 1,365 from A, 1,365 from B and 1,366 from C, in one illustration), and product:4821 moves because slot 13107 is in the range taken from C. Differences matter. On a ring ownership follows from positions and tokens implicitly; with slots the mapping is a table you choose and migrate explicitly. Slots are the unit of movement and make multi-key operations well defined: all keys in one slot are on one node, so a tag like {tenant:cedar} puts a tenant's keys together, at the price that a hot tenant becomes a hot slot on one node. The Redis spec also has no proxies and uses asynchronous replication with no merging of values; I would not describe it as consistent hashing in an interview, though both aim at limiting movement when nodes change.

Likely follow-ups
  • What happens to multi-key commands? — They require all keys to be in the same slot; otherwise the node rejects them with a cross-slot error. Hash tags are how you opt in.
  • Why 16,384 and not more? — I would not give a design rationale I cannot cite. What the Redis Cluster specification does state is that the slot count fixes an upper limit of 16,384 primaries, with about 1,000 suggested as the practical maximum.
Swipe to pan
Redis Cluster: 16,384 hash slots mapped to nodesFixed Slot Table: 16,384 Slots, Moved Range by RangeHASH_SLOT = CRC16(key) mod 16384 · illustrative even split, real planners choose their ownBefore — 3 primariesA 0–5,460B 5,461–10,922C 10,923–16,383After — D added by migrating 4,096 slots (25%)A 0–4,095D 1,365B 5,461–9,557D 1,365CD 1,366C 13,654–16,383slot 0slot 16383{tenant:cedar}:*slot 860 (hash tag): Atenant:cedar:product:4821slot 8298: B, staysproduct:4821slot 13107: C, then DWhy slots instead of a ring• A fixed slot→node table: moving data means moving named slots, one at a time, not shifting hash positions.• Hash tags: only the text inside {…} is hashed, so keys sharing a tag share a slot and may be used in one multi-key command.• Cost of the tag: a whole tenant on one slot is one shard. A hot tenant is a hot slot, and a slot cannot be split.
A fixed table maps 16,384 slots to nodes; resharding moves slots, not arbitrary key ranges.
Q

How does a client recover from a stale shard map?

Strong answer angle

By treating redirection replies as normal protocol, bounded by a retry budget. Suppose the client believes slot 13107 belongs to C, but it has just moved to D. The client sends the command to C and receives a MOVED reply naming slot 13107 and the new address. MOVED is permanent: the client updates its slot map (by patching that slot or refreshing the table from any node) and re-sends to D. The other case is a slot that is mid-migration. The source node still answers for keys it has; for a key already transferred it replies ASK, a one-time redirect. The client then sends ASKING followed by the command to the target, which accepts it only because of that prefix, and does not update its map, since the slot has not finished moving. A well-behaved client therefore keeps a local slot table, handles both replies, caps the number of redirects per command (say 3–5), applies an overall deadline, and backs off if it keeps being redirected, because a loop usually means the cluster itself is unstable and hammering it makes things worse. Timeouts are the harder case: a request to a dead node says nothing about where the slot lives, so the client should refresh topology from another node, and it must remember that after a timeout the previous attempt may or may not have been applied, which is harmless for a read or an idempotent SET and dangerous for a retried increment or compare-and-set unless guarded by a token.

Likely follow-ups
  • Client library or proxy? — A smart client avoids an extra hop and has the map locally, but every language library must implement redirect handling correctly. A proxy hides topology from clients and centralises limits, adds a hop, and becomes another thing to run and scale.
  • How often should the map be refreshed? — On MOVED, on connection errors, and periodically in the background, so a slow drift is caught even without errors.
Q

How would you migrate safely from a single cache to a sharded one?

Strong answer angle

Treat it as a staged rollout in which the old cache stays as the fallback and the database never sees a cliff. First change the key schema so that the new cluster is addressed by a new prefix or version (the v2: segment is there for this reason): old and new data can never be mistaken for each other. Second, build the new cluster beside the old one and run shadow traffic: the application still serves from the old cache but also fills and reads the new one, compares results and records hit rate, latency and mismatches without affecting users. Third, warm it. A cold 15 GiB cluster pointed at live traffic would send the miss path straight to the database, so replay hot keys or fill through shadow traffic until the hit rate approaches the old one's. Fourth, cut over gradually behind a flag: 1% of pods or tenants, then 10%, then everyone, watching hit rate, database QPS against the 19,200 per second ceiling, error rate and p99. Keep the old cache populated and writing during the window so rollback is a flag flip rather than a rebuild. Invalidation needs special care, since both caches must be invalidated until the old one is retired, or the old one will serve stale data after a rollback. Finally drain and remove the old cache once a full TTL cycle has passed with no reads. The usual mistakes are cutting over cold, forgetting dual invalidation, and having no rollback because the old cache was switched off at the same time.

Likely follow-ups
  • Can you migrate gradually per key range? — Yes: with slots or a prefix, route a subset of keys to the new cluster first. It is a finer-grained version of the same plan.
  • What if the new cluster has different client semantics? — Verify with shadow reads before real traffic: key length limits, serialization, and multi-key operations are the common surprises.
Chapter 09

Replicas, Failover and Surviving an Outage

Replication protects availability, not truth. The first two questions are about exactly what an asynchronous replica promises; the last two about how the rest of the system behaves when the cache cannot answer.

Q

What guarantees do asynchronous replicas actually provide?

Strong answer angle

Fewer than people assume. In Redis, replication is asynchronous by default: the primary applies a write, acknowledges the client, and streams the change to its replica, which reports the offset it has processed, also asynchronously. The primary does not wait. What you get is a copy that is usually close behind, a replica that can take over if the primary fails (with a partial resync using a replication ID and offset when possible, or a full resync from a snapshot when not), and the option of reading from replicas that may be stale. What you do not get is any promise that the replica has a given write at a given moment, so a read from a replica can return an older version, and no promise that a promoted replica contains everything the old primary acknowledged. The cluster documentation says it tries on a best-effort basis to retain writes from clients in contact with the majority of primaries, and that there are small windows where acknowledged writes can be lost, with larger windows in a minority partition. A WAIT command can block until a number of replicas acknowledge, which narrows the loss window but does not make the system strongly consistent. Memcached, by contrast, has no built-in replication; copies are a client or proxy concern. For a cache the usable summary is this: replicas improve read capacity and recovery time, they shorten the cold-start that follows a primary loss, and they never make the cache a safe place for the only copy of anything.

Likely follow-ups
  • Should reads go to replicas? — For data that tolerates a little extra staleness, yes: it spreads read load. Not for data that was just written and must be read back, nor for anything in the never-stale classes.
  • What does replica lag look like in practice? — It can be anything from negligible to seconds under load or resync; the illustration in this guide uses 120 ms. Monitor the offset gap instead of assuming it.
Q

How could a failover lose an acknowledged cache write?

Strong answer angle

Here is the timeline, with a hypothetical replica lag of 120 ms. At time t the cache write that reflects the price change for product 4821 (a $109 value, version v6, or the delete that removes the old entry) goes to primary C. C applies the write and replies OK, so the client believes it succeeded. The replication stream is still carrying the update; the replica C' has not applied it and holds $99, v5. Within that window, C crashes. The cluster elects C' as the new primary. It does so from the state it has, which does not include the write, and no process holds that write any longer; when the old primary eventually returns it is demoted and its extra data is discarded (the spec describes last failover wins and no merging of values). The cache is now serving $99 for a product whose price is $109 in the database. For a cache this is a bounded annoyance: the lost write is not replayed by anything, so only a later event for the same product or the TTL (at most about 360 s) repairs it, and checkout revalidates. A team that cannot wait that long can replay the last few minutes of invalidation events after every failover. If the cache had been the only copy of that price, the write would simply be gone, a business error rather than a cache blip. WAIT would shrink the window by waiting for the replica before replying, at the price of write latency, but it would still not guarantee survival in every failure. The defensible design is not to stake anything on the answer: keep the authority elsewhere, and make cache writes repairable.

Likely follow-ups
  • Could you use synchronous replication? — Waiting for acknowledgements trades latency and availability (the write stalls if the replica is unreachable) for a smaller window. It is rarely worth it for a cache; it is worth it for the system of record.
  • How would you notice lost writes? — By sampling cache values against the database's record versions and alerting on mismatches after failovers.
Q

When should a client use stale data, fail closed, or call the database?

Strong answer angle

Decide by data class before the incident, because during one there is no time. Descriptive content (C1) may be served stale for a defined period; stale-while-revalidate allows up to 5 minutes beyond the 15-minute TTL. A price on the card (C2) can usually be shown from a stale or last-known entry during a cache impairment, with the checkout path revalidating, since the displayed value is non-binding. Availability badges (C3) can go stale for their short 30-second budget or be hidden. Inventory reservation, payment instruments and permission decisions (C4) are never answered from the cache's stale copy: for these, call the authority directly or fail closed, so an unauthorised request is refused rather than allowed because of an old entry. The third option, calling the database, is limited by what the database can take. A cache outage at peak redirects 48,000 eligible requests per second to the database, but after the 12,000 that were never eligible, only 7,200 per second fit under the planned 19,200 ceiling. So the database is a choice for a thin slice, usually the classes where staleness is unacceptable or the cheapest-to-compute ones, and the rest is stale service, degraded responses or shedding. Use circuit breakers so repeated failures stop being attempted, and timeouts with retries that have jitter and budgets, because naive retries add load to a database that is already failing.

Likely follow-ups
  • Which class would you shed first? — The least valuable work: pages that can be degraded (hide recommendations, show cached descriptions) before anything that checkout needs.
  • Who owns the policy? — The product and engineering together, written per class and tested, not improvised on call.
Q

How do you protect a database during a total cache outage?

Strong answer angle

Start from the arithmetic, because it dictates the strategy. If the cache disappears at peak, up to 48,000 eligible requests per second go to the database in addition to 12,000 that never used the cache. A database with a hypothetical 24,000 reads per second capacity and a planned 19,200 ceiling can accept about 7,200 more: the eligible load is 6.7 times what is admissible, and 40,800 of every 48,000 requests, 85%, must be served some other way. Over one minute that is 2,880,000 extra requests, of which about 2,448,000 must be answered some other way. The defences work in layers. Admission control caps database concurrency to the 7,200 per second figure with a queue that has a deadline, rejecting the rest quickly rather than letting latency climb. Single-flight collapses duplicate reads of the same key, so the load is driven by distinct keys, not requests. Local L1 caches and stale-if-error serving absorb the hot keys and the permitted classes. Circuit breakers on the cache client avoid a long timeout on every request while the cache is down. Degraded modes drop expensive, non-essential features first. On recovery, do not let a cold cache and a thundering herd return together: warm the hottest keys first, ramp traffic back, and keep the admission cap until the hit rate has recovered. A replica per shard shortens the outage when one node fails; it does nothing when the whole cluster is unreachable, so the plan must exist anyway.

Likely follow-ups
  • Is a circuit breaker on the database also needed? — Yes: when the database slows, stop sending more work, return degraded responses, and probe gently for recovery.
  • How would you test this? — In a game day: disable the cache at realistic load in a staging environment, confirm the admission cap holds, and measure how long recovery takes.
Chapter 10

Hot Keys, Tenants, Regions and What Not to Cache

Skew, isolation, geography and security are where a cache that works in a demo most often fails in production.

Q

How do you detect and handle a single hot key?

Strong answer angle

Spot it by looking at the distribution, not the average. Per-shard operations per second is the first signal: with 48,000 GETs per second at peak and four shards, a uniform spread gives 12,000 each. If product 4821 draws 18% of eligible reads, 8,640 per second land on one key, and its shard carries 8,640 + (48,000 − 8,640) ÷ 4 = 18,480 operations per second, 73.9% of a hypothetical 25,000 budget, while the others carry 9,840, a skew of 1.88. If a trending SKU took 45% (21,600 per second), its shard would need 28,200 and exceed the budget while the other three idle at 6,600. Detect with per-key counters sampled in the client or proxy, top-N reports, or the engine's own sampling tools; check the vendor documentation for what is available. Adding shards does not help, because the key still has one owner. What helps is serving the key from more places: a short-TTL local L1 in each pod (with 20 pods and a 2 s TTL, at most 20 ÷ 2 = 10 shard reads per second for that key), or replicated read-only copies under several derived keys with the reader picking one at random. Both reintroduce staleness, so invalidation has to reach the copies, and the TTL has to be the staleness the business accepts for that product (two seconds of old price on one item). Writes to a hot key are a different problem, since replicating copies does not apply; split the counter or buffer updates. Promotion can be automatic: a rolling counter promotes a key into the hot set and demotes it after the burst.

Likely follow-ups
  • Would you pre-split the hot key by default? — No; it adds invalidation cost to every key for a problem a few have. Detect and promote.
  • What if the hot key is a write-heavy counter? — Shard the counter into N sub-keys and sum on read, or aggregate in memory and flush periodically, accepting delay.
Q

How do tenant boundaries affect keys, quotas and observability?

Strong answer angle

Three layers, none of which a naming convention alone secures. Keys: the tenant belongs in the key (v2:cedar:product:4821), so that Cedar and Birch cannot collide, and the value should also record its tenant so a mix-up is detectable on read. But a prefix is not authorization. The service has to derive the tenant from the verified identity, never from a request field, and the cache should enforce access through its own accounts where it supports them; otherwise anything that can reach the cache can read every key. Placing a tenant's keys in one hash slot with a hash tag simplifies multi-key use but concentrates its load on one node, so a noisy tenant (Cedar, in the example) becomes a hot slot. Quotas: the cache is a shared pool, so cap memory per tenant (and entry counts for negative entries), or one tenant's large catalog evicts the others' hot data. Where one tenant is large enough to matter, a dedicated shard tier isolates it at the price of lower utilization; an alternative is weighted eviction or per-tenant limits in the application. Observability: break hit rate, memory, evictions and latency down by tenant, because a healthy global average can hide Birch's hit rate collapsing as Cedar floods the cache. Be careful that tenant identifiers in metrics and logs are not themselves sensitive. The classic incident is caching a response that depended on the caller's identity under a key that omitted it, which serves one tenant's data to another.

Likely follow-ups
  • Should each tenant get its own cache cluster? — Only where compliance or noise justifies the cost; the default is a shared pool with quotas and monitoring.
  • How do you test isolation? — Automated tests that request a key as tenant A for an object owned by tenant B, expecting a miss or a denial, plus a canary that alerts on cross-tenant reads.
Q

How does a multi-region cache handle invalidation and GDPR or residency restrictions?

Strong answer angle

Each region runs its own cache close to its API pods, because a cross-ocean cache read would cost more than it saves. That choice makes invalidation a distribution problem. A write commits in the authoritative database, the outbox emits an event, and the event is delivered to a subscriber in each region (EU and US) that deletes or refreshes the key. Delivery takes time and can fail, so freshness is bounded, not instant: normal propagation is seconds, a lost event is bounded by the TTL, and a partitioned region keeps serving what it has. The design therefore states a staleness budget for cross-region visibility and does not claim that all regions agree at once. For residency, the question comes first: which data may leave which region at all? Data that must stay in the EU should never be written to a US cache, which means the key and the invalidation stream must be region-aware, replication of cache contents across regions should be off for that class, and each region fills from its own authoritative copy. Erasure requests are a second case: deleting a person's data must also delete or expire cached copies, so the deletion event must reach every regional cache and any L1, and the TTL gives an upper bound while the event is the mechanism; log that the purge happened. Legal specifics belong to your compliance team; the engineering contribution is knowing exactly where each copy can live and how it is removed.

Likely follow-ups
  • Why not one global cache? — A single region's cache adds cross-region latency to every hit and a cross-region dependency to every request; regional caches fail independently.
  • What happens in a partition between regions? — Each region continues with its local cache and bounded staleness, and the invalidation backlog drains when the link recovers; the TTL protects against a lost backlog.
Q

What should not be stored in a cache for security or correctness reasons?

Strong answer angle

Think in three groups. Secrets and sensitive data: payment instruments, card numbers, passwords and tokens should not sit in a shared in-memory store, where a memory dump, a debug command or a misconfigured network exposes everything at once. If a token must be cached, store the minimum, encrypt it, set a short TTL and give it its own isolated instance. Authoritative decisions: inventory reservations (a cache entry cannot reserve stock), authorization or permission results served by TTL alone (a revoked permission would continue working until expiry), and anything whose wrong answer causes money or access to be given away. Identity-dependent responses cached under identity-independent keys: a page personalised for one user, a response containing another tenant's data, and a 403 or 401 stored as not-found for everybody. Negative caching needs its own restraint: a 30-second not-found entry is safe for an object that does not exist, but never cache an authorization failure across identities, and cap the number of negative entries per tenant so that an attacker enumerating random IDs cannot fill the cache or poison it. Finally, do not store anything you cannot rebuild: if losing the entry would be a business error, you have built a database, and it deserves one.

Likely follow-ups
  • Can you cache permissions at all? — Briefly and safely, with a versioned revocation epoch: a cached permission is honoured only if its epoch matches the authority's current one.
  • Is encrypting values in the cache enough? — It limits exposure, but it does not fix a wrong key design or missing access control, and it adds CPU and key management.
Chapter 11

Testing, Scale Tiers and the Durability Question

The last three questions are about proving the design works, how little of it you need at modest scale, and why the cache must not quietly become the database.

Q

How do you test the cache system under node failures and skew?

Strong answer angle

I would test in four layers. At the bottom, unit and property tests on the logic that is easy to get wrong: the conditional fill must reject a lower version, single-flight must deliver one load and many results, TTL jitter must stay inside 240–360 s, and the key builder must include tenant and schema version. Next, integration tests with a real cache cluster in a container, exercising redirects: move a slot while a client is reading and check that MOVED and ASK are handled within the retry budget. Third, load tests with a production-shaped distribution rather than uniform keys. Replay a sampled access log, or generate a Zipf-like skew (a few keys take most reads, the rest a long tail) with a dominant key at 18% of reads, and look at per-shard load, not only the total: a uniform test would never expose the 18,480-versus-9,840 imbalance. Fourth, fault injection in a pre-production environment at realistic load: kill a primary and measure time to recovery and which writes were lost; partition a replica; add latency; fill memory; flush the whole cache and verify the admission cap protects the database; expire many keys at once; drop invalidation events and confirm the TTL bounds staleness. Each experiment needs a hypothesis and a pass condition, for example that the database stays under 19,200 reads per second during a full flush and the p99 for degraded classes stays within a stated limit. Record the findings and rerun them after major changes; a game day (a rehearsed, deliberate failure exercise) that runs once goes out of date.

Likely follow-ups
  • How would you test the race condition? — A deterministic test that pauses the loader between the database read and the fill, commits a write and an invalidation, then releases the loader and asserts that the old version is rejected.
  • What would you monitor in production to catch what tests missed? — Hit rate by class and tenant, evictions, per-shard operations, replica lag, redirect rate, database QPS with an alarm below the ceiling, and a sampled version-mismatch counter.
Q

What is the minimum viable implementation at 1,000 requests per second versus 100,000?

Strong answer angle

At 1,000 requests per second, build very little. A single Redis or Memcached instance (plus a replica if restart time matters), cache-aside in the application, a TTL on every key with a little jitter, delete-on-write invalidation in the same code path that updates the database, and a size cap. In-process single-flight is a few lines and worth having. At 1,000 requests per second a database probably copes with the miss load, a cache outage degrades rather than destroys, and sharding, slots, regional caches and distributed leases would be cost without benefit. Watch hit rate and evictions and do not build the rest until the data demands it. At 100,000 requests per second, the same ideas hit limits. One node's operation budget (a hypothetical 25,000 per second in this guide) is exceeded, so shard, with slots and a client that follows redirects. The database cannot survive a cold cluster, so you need admission control, stale-if-error and a warm-up procedure. Skew becomes a certainty, so you need hot-key detection and L1 for a few keys. Replicas and failover matter because losing a shard is a regular event, and the invalidation path moves to an outbox and a stream so that it is reliable and observable. Multi-tenancy brings quotas and per-tenant metrics, and several regions bring a staleness budget. Between the two, add each piece when a measured symptom calls for it, in the order the symptoms appear: usually stampedes first, then a hot shard, then capacity.

Likely follow-ups
  • What is the first thing you would add after the minimum? — Per-class hit-rate and eviction dashboards; you cannot tune what you cannot see.
  • At what size do you shard? — When one node's memory or operation budget is within reach, or when losing it would be unacceptable; the numbers in the capacity exercise are the method, not the answer.
Q

Why shouldn't a cache be treated as a durable database?

Strong answer angle

Because everything that makes a cache effective is a reason it can lose data. It runs in memory with an eviction policy that deletes entries to stay under a limit, so a key you wrote can vanish tomorrow even before its TTL. Expiry deletes by design. Replication is asynchronous, so a failover can drop acknowledged writes, which Redis Cluster's own documentation describes. If persistence is turned on, snapshots and logs reduce the loss but do not turn it into a database with transactions across keys, backups, schemas, queries and audited recovery; atomicity is per command on a single node, so there is no cross-key or cross-shard transaction. Stale and missing entries are accepted by the design because the authority can rebuild them, and that is the property to protect: if the system would break when an entry is missing or older than the database, the cache has been made load-bearing for correctness. Write-behind is the usual path to that mistake: acknowledging users before the database has the data makes every cache failure a lost write. The test I would apply is whether you could flush the entire cache at 3 a.m. and have the product recover without data loss, only with higher latency and a controlled database load. If not, move that data into something durable and keep a cache in front of it.

Likely follow-ups
  • Is there a legitimate durable use of Redis? — Yes, as a database in its own right with its persistence and replication configured and tested for that purpose; that is a different deployment from a cache and needs a different contract.
  • What about session storage? — Sessions are often kept in such stores; decide what losing them costs (users must log in again) and set persistence and replication accordingly, or keep them in a database.
Chapter 12

Trade-off Rubric: What Separates Good From Excellent

Facts on their own earn little credit; the stronger signal is how a candidate moves between a mechanism, its failure and its price. The table describes three levels of the same skill.

DimensionAdequateStrongExcellent
Cache rolePuts a cache in front of the database.States that the database is the authority and the cache is a disposable copy.Classifies data by tolerated staleness and names what is never cached (reservations, secrets, permissions).
FreshnessSays use a TTL and invalidate on write.Explains the refill race and a versioned conditional fill.Gives the worst-case staleness number (about 360 s), where the version comes from, and why checkout revalidates.
Load and numbersMentions that a cache reduces load.Derives database QPS with and without it.Separates hit rate from backend reduction (90% versus 72%), states assumptions, and checks the fallback capacity after an outage.
ShardingSays hash the key to a node.Contrasts modulo with a ring or slots and the cost of resharding.Explains slots, hash tags, MOVED and ASK, and that a hot key defeats any placement scheme.
ReplicationAdds replicas for availability.Knows that asynchronous replication can lose acknowledged writes.Ties the loss to the data class, explains WAIT's limits, and says what repairs the entry.
FailureMentions a fallback to the database.Computes the admissible fallback rate and sheds the rest.Defines per-class behavior (stale, degrade, fail closed), recovery ramp-up and a test that proves it.
CommunicationLists components.Adds boxes in response to named weaknesses.Marks assumptions, invites a different workload and says what would change the answer.
Use it for self-review: after a practice answer, mark where you landed in each row.
Chapter 13

Mistakes That Change the Assessment

These are the errors that turn a competent answer into a doubtful one. The first list is about claims, the second about process.

  • Saying the cache is always consistent with the database because writes invalidate it. The refill race, lost events and replica lag all leave old values behind; only a TTL or a version check bounds them.
  • Treating a TTL as synchronization. It is an upper bound on how long an entry may be served.
  • Quoting a 90% hit rate as a 90% reduction in database load. Non-eligible traffic never benefits (72% in the worked numbers).
  • Calling modulo hashing, a consistent-hash ring and Redis Cluster slots the same thing. They differ in how ownership is defined and moved.
  • Adding shards to cure a hot key. One key has one owner.
  • Promising that replication or WAIT means no acknowledged write is ever lost.
  • Describing a stampede fix as a lock only. Locks need expiries, deadlines and a fallback, and do nothing for many keys expiring together.
  • Giving payload bytes as the memory requirement (11.44 GiB) with no overhead, replica or headroom.
  • Putting secrets, reservations or permission decisions in a TTL-only cache.
  • Claiming zero stale reads, no downtime or guaranteed sub-millisecond latency.
  • State assumptions and mark numbers as illustrative before doing arithmetic.
  • Introduce each component when you can name the weakness it fixes.
  • Say what each failure does to the user, the database and the data, then how you would detect it.
  • Offer the simpler design first and say which measurement would make you add the next piece.
  • Keep GiB and GB, mean and p99, hit rate and backend reduction apart.
  • Distinguish what you verified in the documentation of a specific engine from what is general practice.
Chapter 14

Follow-Up Challenges and Curveballs

Interviewers often change one assumption and watch what you re-derive. Practice with these.

CurveballA defensible response
The hit rate drops from 90% to 80% after a deploy. What happens to the database?Misses double from 4,800 to 9,600 per second at peak, so database load becomes 9,600 + 12,000 = 21,600 per second, above the 19,200 ceiling and near the 24,000 capacity. Look for a key-schema change (a new prefix means a cold cache), a shorter TTL, or an eviction-rate rise before blaming traffic. Roll back or warm the new keys, and cap admission while you investigate.
One pod shows a stale price for a minute while others are correct.Suspect an L1 that missed an invalidation or a subscriber that is lagging. Check that pod's event offset and local TTL; the backstop TTL should clear it within seconds, so a minute means the local TTL is too long or the subscription is broken.
Add a fifth shard to a four-shard cluster under modulo placement.About 80% of keys change owner (N ÷ (N+1) = 4 ÷ 5), so you create a near-cold cache. Move to slots or a ring, or schedule the change off-peak with warming and an admission cap.
The interviewer says the cache stores a user's saved payment method for speed.Push back: payment instruments are in the never-cache class. Offer a token reference held by a payment service, with the cache holding only non-sensitive display data.
A flash sale makes one SKU 45% of eligible reads.Its shard would need 28,200 operations per second against a 25,000 budget while the others sit at 6,600. Promote the key to per-pod L1 with a short TTL or replicated copies, and note the staleness cost.
Replica reads are cheap, so should all reads use replicas?Only for classes that tolerate lag. Replica lag is not bounded by the protocol, and a read just after a write may return the older value.
Can you guarantee a deleted product disappears everywhere immediately?No. You can bound it: the delete event reaches each regional cache and L1 within seconds normally, and the TTL (240–360 s for prices, longer for descriptions) caps the worst case when an event is lost.
Why not set TTL to a few seconds and skip invalidation?Every key would miss every few seconds, raising database load and stampede risk, while still serving stale data inside that window. Invalidation gives low staleness without paying for constant misses.