Design a distributed cache (like Redis)
The interviewer asks
“Design a distributed in-memory cache that application servers use to store key-value data with TTLs. It must hold about 10 TB of data, serve millions of reads per second with sub-millisecond latency, and keep working when nodes fail.”
What a strong answer looks like
A strong answer starts with the single-node building block (hash table + eviction policy + TTL, single-threaded event loop or sharded locks) and then scales out: partition keys across nodes with consistent hashing or fixed hash slots, replicate each shard to a replica for availability, and decide how clients find the right node (smart client vs proxy). Be explicit that a cache is not the source of truth: replication is asynchronous, a failover may lose recent writes, and that is accepted. Then go deep on the real problems: hot keys, cache stampede on expiry, rebalancing without mass misses, memory fragmentation and eviction, and keeping the cache consistent with the database (cache-aside with delete-on-write, short TTLs, versioning). Senior candidates discuss failure detection and split brain, and when you need persistence at all.
Concepts to mention
1 Requirements
Functional
- GET, SET (with TTL), DELETE on string keys with binary values up to 1 MB
- Atomic increment and compare-and-set (for counters and simple versioning)
- Automatic expiry of keys by TTL
- Eviction when memory is full (LRU or LFU, configurable)
- Add and remove nodes without downtime
- Batch operations (MGET/MSET) for fewer round trips
Non-functional
- Latency p99 under 1 ms in the same availability zone, p999 under 5 ms
- Throughput: 5M reads/s and 500k writes/s across the cluster
- Capacity: 10 TB of hot data
- Availability 99.99%; a node failure recovers in under 30 seconds
- Loss of recent writes on failover is acceptable (cache, not database)
- Horizontal scaling: add capacity by adding nodes
Out of scope (say it out loud)
- Durable storage guarantees and complex data types (lists, sorted sets) beyond mention
- Multi-key transactions across shards
- Cross-region active-active replication (discuss briefly)
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
RESP/TCPGET key | Return value or nil. Client routes to the owner shard by hash slot. |
RESP/TCPSET key value [EX seconds] [NX|XX] | Store value with optional TTL; NX = only if absent (used for locks and dedupe). |
RESP/TCPDEL key / MGET k1 k2 … | Delete, and batched reads grouped per shard by the client. |
RESP/TCPINCRBY key n / CAS key expected new | Atomic counters and optimistic updates. |
AdminCLUSTER SLOTS / MOVE SLOT | Clients fetch the slot map; operators move slots between nodes when scaling. |
4 Data model
Key-value entriesIn-memory hash table per nodekey bytesvalue bytesexpire_atLRU/LFU bitsflagsIncremental rehashing (two tables during resize) avoids long pauses when the table grows.
Expiry indexIn-memory per nodesubset of keys with TTLLazy expiry on access plus an active sampler that checks ~20 random TTL keys many times per second, so expired keys do not pile up.
Slot mapCluster metadata (gossip or etcd/ZooKeeper)16384 hash slots → primary nodereplicasconfig epochCRC16(key) mod 16384 gives the slot. Moving a slot moves a small slice of keys, not a full rehash.
Optional snapshotLocal disk / S3RDB-style snapshotappend-only logOnly for warm restarts; reduces cold-start misses after a full restart.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Read with cache-aside
- 1App computes slot = CRC16('user:42') mod 16384 and looks up the owner in its cached slot map.
- 2It sends GET to that primary (or a replica, if stale reads are allowed) over a pooled TCP connection.
- 3On a hit (~0.3 ms) it returns the value.
- 4On a miss it takes a short lock (SET lock:user:42 NX EX 5) so only one caller loads from the DB; others wait briefly or serve stale.
- 5The loader reads the DB, then SETs the value with TTL 10 min plus random jitter of ±10%.
- 6If the node replies MOVED (slot migrated), the client refreshes its slot map and retries once.
Node failure and failover
- 1Shard 2 primary stops answering heartbeats.
- 2Other nodes (or the etcd-based controller) mark it as suspected, and after a majority agrees within ~5-15 s mark it failed.
- 3Its replica is promoted with a higher config epoch, and the new slot map is published.
- 4Clients get connection errors or MOVED, refresh the slot map and route to the new primary.
- 5Writes acknowledged by the old primary but not yet replicated are lost; the app sees misses and reloads from the DB.
- 6A new replica is provisioned and syncs from the new primary (full snapshot then stream).
7 Deep dives
Partitioning: consistent hashing vs hash slots
Simple 'hash(key) mod N' breaks when N changes: going from 160 to 161 nodes remaps almost every key, causing a mass miss storm that can take down the database. Consistent hashing places nodes on a ring (with 100-200 virtual nodes each to smooth load) and a key goes to the next node clockwise; adding a node moves only about 1/N of keys. Redis Cluster uses a variant: a fixed 16,384 hash slots, with each slot assigned to a primary. Scaling means moving whole slots, key by key, with the source answering ASK redirects for keys already moved. Fixed slots make the routing table small and explicit, make rebalancing an operator decision (not an accident of hashing), and allow hash tags like {user:42} to force related keys onto the same slot for multi-key operations. Memcached-style systems instead put consistent hashing in the client library (ketama), keeping servers completely unaware of each other. In an interview, either is fine; explain why plain modulo fails and how many keys move when you add a node.
Eviction, TTL and memory
When memory reaches the limit the cache must evict. Exact LRU needs a linked list over 60M keys per node and costs memory and pointer updates on every read. Redis uses approximate LRU: each key holds a 24-bit access clock, and on eviction it samples ~5-10 random keys and evicts the oldest, keeping a small pool of good candidates. This is close to true LRU at a fraction of the cost. LFU (frequency with decay) is better when a big scan of one-time keys would otherwise push out the real hot set. Expiry is lazy (check on access) plus active: sample random keys with TTL and delete expired ones, repeating while more than ~25% of the sample was expired. Memory fragmentation from many different value sizes is real; allocators like jemalloc with size classes, and active defragmentation, keep RSS close to used memory. Plan nodes at ~50-60% of RAM because snapshotting with fork() uses copy-on-write, and write-heavy nodes can nearly double memory during a snapshot.
Hot keys and cache stampede
Two problems take down caches in production. Hot keys: a celebrity profile or a flash-sale product gets 500k reads/s, all on one shard, saturating its CPU or NIC. Fixes: a small in-process cache in each app server with a 1-5 s TTL (absorbs most reads), reading from replicas, or key replication: store the value under user:42#0..#9 on different shards and read a random one. Detect hot keys with client-side sampling or the server's hot-key stats. Stampede: a popular key expires and 2,000 requests miss at the same moment and all query the database. Fixes: request coalescing (single-flight per key in each app server), a distributed lock so one caller recomputes while others serve the stale value, TTL jitter so many keys do not expire together, and probabilistic early refresh (refresh a little before expiry with a probability that grows as expiry approaches, the XFetch technique). Mention that after a cold start or a big failover, you should warm the cache or rate-limit DB reads, otherwise the cache outage becomes a database outage.
8 Trade-offs
Smart client vs proxy
Smart clients (hold the slot map, connect directly to every shard) give the lowest latency: one hop. But every language needs a good client, and 5,000 app servers × 320 nodes means 1.6M connections. A proxy layer (Twemproxy, Envoy Redis proxy) hides topology and pools connections, costing an extra hop (~0.2 ms) and more machines. Large fleets often use a proxy or a sidecar for this reason.
Async vs sync replication
Synchronous replication would double write latency and make the cache as slow as a database. Since the cache is not the source of truth, asynchronous replication is accepted: a failover can lose the last few milliseconds of writes. If a use case cannot tolerate that (sessions, rate-limit counters for security), use WAIT-style acknowledgement for those keys only, or store them in a durable store.
Cache invalidation strategy
Cache-aside with delete-on-update (update DB, then DELETE the key) is the common default. Updating the cache instead of deleting can race: two writers can leave the older value cached. Delete-then-reload still has a small window where a slow reader writes an old value back; short TTLs bound it, and versioned values (SET only if version is newer) or invalidation from the DB's change stream (CDC → delete) close it.
9 Bottlenecks
Single-threaded node CPU
A Redis-style node runs commands on one thread, so one slow command (KEYS *, a big DEL, a large Lua script) blocks everything for milliseconds. Ban O(N) commands, use UNLINK for async deletes, and use I/O threads for network handling. Scale by more shards, not bigger nodes.
Connection count
Tens of thousands of clients each opening connections to hundreds of nodes exhausts file descriptors and memory. Use connection pooling, multiplexed clients and proxies or sidecars.
10 Failure handling
Network partition and split brain
If a primary is cut off from the majority but still reachable by some clients, it may keep accepting writes that will be lost when the replica is promoted. Redis Cluster limits this with node timeout: a primary in the minority side stops accepting writes after it cannot reach the majority. Writes in that window are lost, which is acceptable for a cache.
Whole cache cluster lost
All reads miss and go to the database, which usually cannot take 5M reads/s. Protect the DB with rate limiting and load shedding, warm the cache from snapshots or by replaying recent hot keys, and design apps to degrade (serve defaults, hide non-critical widgets).
11 What impresses
- Explains why mod-N hashing causes a miss storm and how many keys move with consistent hashing
- Sizes nodes with headroom for fork/copy-on-write and fragmentation
- Describes Redis-style sampled LRU instead of a naive linked list
- Covers stampede with several layered fixes, including probabilistic early refresh
- Explains the delete vs update race and versioned writes or CDC invalidation
- States clearly that losing a few writes on failover is acceptable and why
12 Mistakes
- Using hash(key) mod N with no plan for adding nodes
- Synchronous replication or 'strong consistency' for a cache without reason
- Ignoring hot keys and stampedes
- Treating the cache as the source of truth with no DB fallback
- Filling nodes to 100% of RAM
- Updating the cache value on writes and not seeing the race condition
13 Follow-ups
Lessons this design uses
Practice questions
- Estimate how much Redis memory you need to cache user profiles for 50 million users, and how you would set up the cluster.
- Your app runs in Mumbai and Hyderabad regions, each with its own Redis. The database primary is in Mumbai with a replica in Hyderabad. How do you keep both caches correct?
- You update the database and then delete the Redis cache key, yet a stale value stays in the cache for hours. How can that happen and how do you prevent it?
- Your cache cluster uses hash(key) mod N to pick a node. When you added one node to go from 10 to 11, the hit rate dropped to almost zero and the database got overloaded. Explain and fix it.
- Your cache cluster restarts during peak traffic. The database gets overloaded, and even after the cache is back, the system stays down for an hour. Explain what is happening and how you would recover and prevent it.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.