Design problems
Classic building blocksHardSDE-2 35 min

Design a distributed cache (like Redis)

Asked at
AmazonFlipkartGoogleMetaMicrosoftSwiggy

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

consistent hashing or hash slotsprimary-replica with async replicationapproximate LRU / LFU evictionlazy plus active TTL expirycache-aside and invalidation on writehot key mitigation (local cache, replicas, key splitting)cache stampede: single-flight, locks, jitter, early refreshfailover and failure detectioncache is not the source of truth

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

Data per node~64 GB usable on a 128 GB nodeKeep ~50% headroom for fragmentation, replication buffers, fork/copy-on-write during snapshots and growth
Number of primaries~160 primaries (+160 replicas)10 TB ÷ 64 GB ≈ 157 → 160 primaries; one replica each → 320 nodes
Per-node load~31k reads/s + 3k writes/s5M reads ÷ 160 = 31k/s; a single Redis-like node handles 100k+ simple ops/s, so we are memory-bound, not CPU-bound
Network per node~250 Mbps average31k reads/s × ~1 KB avg value ≈ 31 MB/s ≈ 250 Mbps; 10-25 Gbps NICs are fine, but large values on hot keys can saturate a NIC
Keys~10 billion10 TB ÷ ~1 KB per entry (key + value + ~50-70 B metadata overhead)

3 API

EndpointWhat it does
RESP/TCPGET keyReturn 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 newAtomic counters and optimistic updates.
AdminCLUSTER SLOTS / MOVE SLOTClients fetch the slot map; operators move slots between nodes when scaling.

4 Data model

Key-value entriesIn-memory hash table per node
key bytesvalue bytesexpire_atLRU/LFU bitsflags

Incremental rehashing (two tables during resize) avoids long pauses when the table grows.

Expiry indexIn-memory per node
subset of keys with TTL

Lazy 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 epoch

CRC16(key) mod 16384 gives the slot. Moving a slot moves a small slice of keys, not a full rehash.

Optional snapshotLocal disk / S3
RDB-style snapshotappend-only log

Only for warm restarts; reduces cold-start misses after a full restart.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAslot 0-5460slot 5461-10922slot …-16383simple clientsasync replicationasync replicationasync replicationon missfetch slot mapperiodic snapshotApp servers (smartclient)Optional proxy (Envoy)Shard 1 primaryShard 2 primaryShard N primaryShard 1 replicaShard 2 replicaShard N replicaSnapshots (S3)Cluster config (etcd)Source-of-truth DB
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Read with cache-aside

  1. 1App computes slot = CRC16('user:42') mod 16384 and looks up the owner in its cached slot map.
  2. 2It sends GET to that primary (or a replica, if stale reads are allowed) over a pooled TCP connection.
  3. 3On a hit (~0.3 ms) it returns the value.
  4. 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.
  5. 5The loader reads the DB, then SETs the value with TTL 10 min plus random jitter of ±10%.
  6. 6If the node replies MOVED (slot migrated), the client refreshes its slot map and retries once.

Node failure and failover

  1. 1Shard 2 primary stops answering heartbeats.
  2. 2Other nodes (or the etcd-based controller) mark it as suspected, and after a majority agrees within ~5-15 s mark it failed.
  3. 3Its replica is promoted with a higher config epoch, and the new slot map is published.
  4. 4Clients get connection errors or MOVED, refresh the slot map and route to the new primary.
  5. 5Writes acknowledged by the old primary but not yet replicated are lost; the app sees misses and reloads from the DB.
  6. 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

Q1How would you add a node with no downtime and few misses?
Q2How do you keep the cache consistent with the database?
Q3What happens if a primary and its replica both fail?
Q4How would you support cross-region caching for a global app?
Q5When would you choose Memcached over Redis?
Q6How do you find which keys are hot in production?

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.

Start free