Design a distributed in-memory cache like Memcached or Redis Cluster that applications use to offload reads from their databases.
Key points
- Shards keys across nodes with consistent hashing in the client or via a proxy, with replicas per shard for availabilityIn plain words: Spread keys over many machines using a ring, and keep a spare copy of each slice in case a machine dies
- Implements eviction (LRU/LFU approximations) and TTLs, and bounds memory per nodeIn plain words: Each machine has a fixed amount of room, so it throws out unpopular items and items past their expiry
- Protects the database from thundering herds on misses with request coalescing, locks or stale-while-revalidateIn plain words: When a popular item is missing, let one request fetch it while the rest wait or get the old value, so the database is not trampled
- Addresses hot keys with client-side caching, replication of hot shards or key splittingIn plain words: For a key everyone wants, keep copies closer to callers or split it across machines
- Defines the consistency contract (cache-aside, invalidation on write) and what happens when a node dies or is addedIn plain words: Say out loud who fills the cache, who clears it on a write, and what happens when a machine joins or leaves
Stages
1Requirements
- get, set with TTL, delete, maybe atomic increment and multi-get, sub-millisecond latency
- Scale to terabytes across nodes, survive node failures, add capacity without a full cache flush
2Estimates
- e.g. 500 GB of hot data at ~1 KB per object is 500M keys, on 64 GB nodes about 10 nodes plus replicas
- 2M gets/s cluster-wide → ~200k per node, within a single node's capability
3API
- get(key), mget(keys), set(key, value, ttl), delete(key), incr(key, n), cas(key, value, version)
- Cluster topology endpoint or client-side ring configuration
4Data model
- In-memory hash table per node, values as byte arrays with expiry and LRU metadata
- Slot or ring assignment mapping key hashes to primary and replica nodes
5High-level design
- Smart client (or proxy like twemproxy/mcrouter) hashes keys to nodes, nodes are independent, replication primary → replica per shard
- Config/coordination service publishes topology, health checks drive failover
6Deep dives
- Eviction with sampled LRU, memory accounting, slab allocation to avoid fragmentation
- Cache stampede prevention, single-flight per key, lease tokens, probabilistic early expiration
- Hot key handling, local L1 cache in the application with short TTL, or replicate the key to multiple shards
7Bottlenecks and failure
- Node loss means a miss storm for that shard, replicas and gradual warm-up protect the database
- Resharding moves keys, consistent hashing limits movement, migrate slots live
- Network partitions, prefer availability, treat the cache as best effort and never the source of truth
Model answer
Requirements
Applications call get, set (with TTL), delete, a few atomic operations and multi-key reads, and expect sub-millisecond responses. The cache must hold terabytes across many nodes, keep working when a node fails, and allow adding capacity without flushing everything. It is a performance layer, not a system of record: losing it must degrade latency, not correctness.
Estimates
Caching 500 GB of hot objects averaging 1 KB means ~500 million keys. With ~50 GB usable per 64 GB node, that is ~10 primaries plus a replica each. Two million gets per second cluster-wide is ~200,000 per node, which a single-threaded Redis or multi-threaded Memcached node can serve.
API
get(key) -> value | nil mget(keys[]) -> values[]
set(key, value, ttl) delete(key)
incr(key, n) cas(key, value, version) // compare-and-swap
cluster.slots() -> [{ range, primary, replicas }]
Data model
Each node is an in-memory hash table from key to { bytes, expiresAt, lruClock }. Cluster metadata maps hash slots (Redis uses 16,384) or ring positions to a primary node and its replicas. Clients cache this map and refresh it on MOVED responses or topology change notifications.
High-level design
Nodes do not talk to each other on the request path; the client (or a proxy such as mcrouter) picks the node. Replication within a shard is asynchronous; on primary failure the replica is promoted and the topology update propagates.
Deep dives
Eviction and memory. Exact LRU costs a linked list per key; Redis samples a handful of keys and evicts the least recently used among them, which approximates LRU well. Memcached uses slab classes to avoid fragmentation. Always set maxmemory and a policy so the node never swaps.
Stampede prevention. When a hot key expires, thousands of requests miss simultaneously and all hit the database. Options: single-flight (one request per key fetches, others wait), leases (the cache hands a token to the first misser and tells others to retry briefly), stale-while-revalidate (serve the expired value while one request refreshes), or probabilistic early expiration so refreshes spread out before the TTL.
Hot keys. A single key read a million times per second saturates one node. Add a short-TTL in-process L1 cache in the application, or replicate the key under several suffixes (key#1..key#N) and read a random one.
Consistency contract. Cache-aside: the app reads cache, on miss reads the database and sets the cache; on write it updates the database and deletes the cache key. Document the staleness window and use leases to prevent the stale-set race.
Bottlenecks and failure modes
- Node loss: the shard's keys all miss until refilled; replicas limit this, and request coalescing keeps the database alive during warm-up.
- Resharding: adding nodes with consistent hashing or slot migration moves only a fraction of keys; migrate slots live and have clients follow
ASK/MOVEDredirects. - Partitions: a cache should favour availability; a partitioned replica serving slightly stale data is better than errors. Never make the cache authoritative for anything.
- Large values: a 10 MB value blocks a single-threaded node; cap value sizes and compress or chunk big objects.
Likely follow-ups
- How does a client discover the cluster topology and react to a node failure?
- When would you choose a look-aside cache versus a read-through cache?