Design a distributed cache
Build the cache that every other design leans on: an in-memory key-value service spread over many machines, answering in well under a millisecond, surviving node failures, and growing without downtime. The model is Redis Cluster (and Valkey, its open-source fork). The interesting parts are routing keys to nodes, failing over without split brain, what clients experience while data moves, and the hot keys and stampedes that hurt the databases behind it. It follows the interview framework phase by phase.
1. The prompt
"Design a distributed cache like Redis or Memcached that many services share. Clients set and get values with expiry times; it must grow beyond one machine."
A cache looks simple until you ask what happens when nodes are added, when one dies, when one key gets a hundred times the traffic of the rest, and when the whole thing restarts empty in front of a database that can't take full load. Those questions are the interview.
2. Frame the problem
Is it a pure cache, or does anyone rely on it as the only copy of data?
A cache: every value can be rebuilt from a database behind it.
Losing a little data on failover is acceptable, so asynchronous replication is fine and persistence is optional.
How much data, and how many operations?
About 2 TB of hot data, and 5M operations a second at peak.
We'll size by both memory and throughput, and see which wins.
Simple strings, or data structures too?
Mostly strings and hashes; some sorted sets and counters.
Redis-style data structures, with operations that run atomically per key.
Do clients need multi-key operations?
Occasionally, on keys belonging to the same user.
Keys that are used together must be able to live on the same node.
How fast?
p99 under 1 ms within a zone.
No extra network hop on the hot path: clients should reach the right node directly.
Must it grow and shrink without downtime?
Yes. Teams add capacity regularly.
Data must move between nodes while clients keep working.
Many teams sharing one cluster?
Yes, several services use it.
One team's huge keys or hot traffic mustn't slow everyone else.
3. What it must do
- A client can set, get and delete values, with an optional expiry (TTL).
- The cache evicts the least useful keys when memory is full.
- Operators can add or remove nodes while clients keep working.
Not now:
- Durable database guarantees: if data can't be lost, it belongs in a database.
- Cross-region replication: each region runs its own cache, warmed from its own database replicas.
- Scripting and pub/sub: they work per node; not core to the design.
4. How well it must do it
| Target | Why this number |
|---|---|
| Very fast: p99 under 1 ms within a zone speed | The cache exists to be faster than the database by orders of magnitude. |
| Scale: 2 TB and 5M operations a second, growing without downtime load shape | Every product feature adds keys and traffic. |
| Self-healing: a failed node is replaced by its replica within ~15 s, automatically surviving failures | During failover, the database behind takes the misses. |
| Never very stale: after failover, at most a few seconds of writes may be lost; never serve data that's minutes old consistency or availability | A cache may lose recent writes, but serving long-stale data breaks features. |
| Protects the database: no failure, restart or expiry may send the database more than ~2× its normal load surviving failures | The worst cache failure is the one that takes the database down too. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Memory | 2 TB ÷ ~100 GB usable per node (headroom for fragmentation and replication buffers) | ~20 primaries would do |
| Throughput | 5M ops/s ÷ ~150K per node | ~35 nodes → so throughput, not memory, sets the shard count: ~40 primaries |
| Availability | One replica per primary, in another zone | → so ~80 nodes in total |
| Cold restart | A 95% hit rate means the database normally sees 5% of reads; an empty cache sends it 100% | 20× its normal load → so we must never restart cold in front of it |
6. The nouns
- Key and value, with a TTL.
- Hash slot: one of 16,384 buckets a key falls into.
- Primary and replica nodes.
- Slot map: which node owns which slots.
- Configuration epoch: a version number that settles which view of the slot map wins.
slot(key) CRC16(hash tag or whole key) mod 16384 slot map slot ranges → primary (+ replica), with a config epoch per owner per node keys in memory, maxmemory + eviction policy, async replication stream to its replica
7. The contract
The Redis protocol over TCP. Clients cache the slot map and handle two kinds of redirection.
GET key → value | nil
SET key value EX 600
DEL key
CLUSTER SLOTS → which node serves which slot range (clients cache this map)
-MOVED 12182 10.0.3.7:6379 (that slot lives elsewhere now: update your map)
-ASK 12182 10.0.3.9:6379 (slot is mid-migration: try there, just this once)
8. First working version
Start with several independent cache servers and let clients pick a server for each key.
8.1 Setting and getting
Each client hashes the key and takes it modulo the number of servers, then talks to that server directly. A get that misses reads from the database and sets the value (cache-aside).
8.2 Eviction
Each server has a memory limit; when it's full, it evicts keys it considers least useful. Redis approximates LRU or LFU by sampling a few keys and evicting the best candidate, which costs far less than a perfect ordering.
8.3 Adding a server
Add it to the list every client uses.
Parked for later (section 9 fixes each)
- Changing the server count remaps almost every key: a near-total miss storm.
- No replicas: a dead server loses its keys and its share of hits.
- Clients must all agree on the server list.
- Multi-key commands across servers don't work.
- Hot keys and expiring popular keys hammer one server and the database.
- A restart starts cold in front of the database.
9. Make it hold up
9.1 Routing keys to nodes scale · protects the database
| Approach | Why | |
|---|---|---|
| ✗ Bad | hash(key) % number_of_nodes in the client | Adding a node remaps almost every key: a near-total cache miss storm that hammers the databases behind it. |
| ~ Good | Consistent hashing in clients (Memcached-style) | Adding a node moves ~1/N of keys. But clients must agree on the node list, and there's no coordinated failover. |
| ✓ Great | Fixed hash slots (16,384) owned by nodes, an authoritative slot map maintained by the cluster, and redirects for clients with stale maps | Moving a slot moves exactly its keys, one slot at a time; clients self-correct via MOVED; failover and resharding are coordinated by the cluster. |
Smart clients or a proxy? Clients that cache the slot map go straight to the right node: no extra hop, which keeps p99 under 1 ms. A proxy tier (Envoy, twemproxy) makes every client simple and hides topology changes, at a little latency and one more tier to run.
9.2 Moving data while clients keep working scale
Adding capacity means moving slots. Press Play to move one slot from node A to node D while clients keep reading and writing.
During the move, keys already copied live on D and the rest on A. A client asking A for a key that has already moved gets -ASK ("try D, just this once"); once the slot is fully moved, the slot map changes and clients with old maps get -MOVED ("update your map").
9.3 Failover without split brain self-healing · never very stale
- Detection: nodes gossip heartbeats. When a majority of primaries agree a primary is unreachable for longer than the node timeout, it's marked failed.
- Promotion: its replica asks the primaries for votes and is promoted with a new, higher configuration epoch, which beats the old owner's claim to the slots.
- Lost writes: replication is asynchronous, so writes acknowledged just before the crash may be lost. For a cache, that's acceptable.
- Minority partitions: a primary cut off from the majority stops accepting writes after the node timeout, which limits how much data diverges before it's demoted. If correctness truly matters, the data doesn't belong only in a cache.
9.4 Multi-key operations speed
A command touching several keys only works if they live in the same slot. Hash tags make that possible: only the part inside {…} is hashed, so {user:42}:cart and {user:42}:profile share a slot (try it in section 13). Use them sparingly: putting too much under one tag creates a hot slot.
9.5 Hot keys, stampedes and big keys protects the database · speed
| Problem | Fix | |
|---|---|---|
| Hot key | One viral item gets 200K reads a second, all on one shard | An in-process cache on app servers with a 1–5 s TTL; copies of the key under several names spread across slots; or read that key from replicas |
| Stampede | A hot key expires and thousands of misses hit the database at once | Request coalescing in the app (one rebuild, everyone waits for it), early probabilistic refresh, or a short lock so one caller rebuilds (Module 09) |
| Big key | A 50 MB list: one slow command blocks a single-threaded node for everyone | Cap value sizes, split collections, and scan for big keys regularly |
9.6 Cold starts, persistence and tenants protects the database
- Cold starts: an empty cache sends the database 20× its normal load (section 5). Options: snapshots (RDB) to restart warm, an append-only file (AOF) for less loss, or no persistence plus a warm-up that preloads hot keys before taking traffic. Persistence costs disk I/O and fork time; many large deployments rely on replicas and warm-up instead.
- Rolling restarts: restart replicas first, fail over, then restart the old primaries, so a shard is never empty.
- Tenants: give large teams their own clusters, or at least separate key prefixes with per-tenant memory budgets and big-key alerts, so one team's mistakes don't evict everyone else's data.
9.7 The whole design after the deep dives
A read with a stale client map, a hot key absorbed locally, and a failover, through the final design.
10. Interviewer follow-ups
Traffic grows 10×.
Add primaries and move slots to them (the slot model makes this mechanical). Push the hottest keys into in-process caches. Split clusters by workload, so one service's spike can't take everyone's latency. Watch the busiest shard, not the average.
Memcached or Redis/Valkey?
Memcached is multi-threaded and simple: strings only, no replication or clustering (clients do consistent hashing). Redis and Valkey add data structures, replication and cluster management. For a shared, self-healing cache, Redis or Valkey; for a simple, very high-throughput string cache, Memcached is still excellent.
How do you keep the cache consistent with the database?
Cache-aside with delete-on-write: update the database, then delete the key, and let the next read rebuild it. Use TTLs as a safety net. For tighter freshness, change data capture from the database can delete keys within a second (Module 09).
One node is consistently slower than the others.
Usually a hot slot, a big key or a noisy neighbour on the host. Check per-slot operation counts and slow-command logs. Move hot slots, split big keys, or add in-process caching for the hot key.
The whole cluster goes down.
Bring it back warm (from snapshots, or a warm-up job), and meanwhile protect the database: rate-limit cache rebuilds, serve stale data where allowed, and shed non-essential features. This is a game-day scenario worth rehearsing.
Should we use a managed service?
Usually yes (ElastiCache, Memorystore, Azure Managed Redis): they handle failover, patching and resharding. Run your own only if you need control over versions, topology or cost at very large scale.
How do LRU and LFU differ in practice?
LRU evicts what wasn't used recently, so a one-time scan of many keys can push out popular ones. LFU evicts what's used rarely, which protects steady favourites but adapts slower to new popular keys. Redis approximates both by sampling.
Cross-region?
Keep caches regional and fill them from regional database replicas. Invalidations replicate with the database's change stream. Cross-region cache replication adds latency and conflicts for little benefit.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| A primary dies | A few seconds of misses or errors for one shard | Gossip; node timeout | Replica promoted with a new epoch |
| Network partition isolates a primary | Writes on the minority side rejected | Cluster state, epochs | Minority primary stops accepting writes after the timeout |
| Hot key | One shard slow | Per-slot ops, latency by node | In-process cache; split or replicate the key |
| Expiry stampede | Database spike | Miss rate and database load jump together | Request coalescing, early refresh, locks |
| Whole cluster restart | Slow pages, database overload risk | Hit rate falls to zero | Warm restart, warm-up jobs, shedding, rebuild rate limits |
| Memory full with wrong eviction policy | Write errors (noeviction) | OOM errors | Use an allkeys policy for caches; alert on memory use |
12. Wrap-up and the clock
Trade-offs we made:
- Hash slots with an authoritative map (coordinated moves) over client-side consistent hashing.
- Smart clients over a proxy (no extra hop).
- Asynchronous replication (fast, may lose recent writes).
- Approximate LRU/LFU by sampling.
- Warm-up instead of heavy persistence.
What we'd watch:
- Hit rate and p99 latency per node.
- Operations per slot.
- Memory and evictions.
- Replication lag.
- Failovers.
- Database load during incidents.
At 10×:
- More primaries.
- Clusters split by workload.
- More in-process caching.
A 45-minute interview
| 0–4 | Frame the problem (pure cache? size? multi-key?) |
| 4–9 | Must do, how well, numbers (ops/s beats memory) |
| 9–12 | Nouns and contract |
| 12–19 | First working version, parked list |
| 19–40 | Deep dives: routing (9.1), resharding (9.2), failover (9.3), hot keys (9.5) |
| 40–45 | Follow-ups and wrap-up |
A 60-minute interview
| 0–5 | Frame the problem |
| 5–11 | Must do, how well, numbers |
| 11–15 | Nouns and contract |
| 15–23 | First working version |
| 23–50 | Deep dives 9.1–9.6 and the final picture |
| 50–60 | Follow-ups: consistency with the DB, cold cluster, managed vs self-run |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Sharding by key, replicas, eviction with LRU and TTLs, cache-aside usage. |
| Senior | Why modulo hashing fails, slots or consistent hashing, client routing and redirects, async-replication loss on failover, hot keys and stampedes, sizing by ops/s. |
| Staff+ | Live resharding (MOVED/ASK), failure detection and epochs, minority-partition behaviour, cold starts and protecting the database, multi-tenant isolation. |
Common mistakes:
- Modulo hashing.
- Sizing by memory only.
- Treating the cache as a database.
- Ignoring the database during failures.
- No answer for hot keys or stampedes.
- Restarting cold.
13. Try it, check yourself, read more
Type a key to see its real Redis Cluster hash slot (CRC16 in the XMODEM variant, mod 16384) and which of 4 nodes owns it. Try {user:42}:cart and {user:42}:profile.
Check: the Redis cluster specification says "foo" maps to slot 12182 and the CRC16 of "123456789" is 31C3; this calculator reproduces both.
What does a hash tag like {user:42} do?
A client receives MOVED 12182 10.0.3.7:6379. What does it mean?
In this design, what decided the number of shards?
A hot key expires and 5,000 requests miss at once. What protects the database?
| Resource | Why |
|---|---|
| Redis Cluster specification | Slots, redirections, failure detection and failover, from the source. |
| Scaling Memcache at Facebook | Running caches at enormous scale: leases, regions, cold clusters. |
| Valkey | The open-source (BSD) Redis fork, with the same cluster model. |