11. Distributed Cache Advanced + Distributed Systems

A. Realistic Interview Prompt

"Design a distributed, in-memory caching system — think Redis Cluster or Memcached — that stores key-value data with sub-millisecond access, scales past what any single machine's RAM can hold, and keeps serving traffic even when individual nodes crash."

This document has already used "put a cache in front of the database" as a supporting move in half its problems (Prerequisites D) — a single Redis instance, treated as a black box. This problem is the opposite framing: you are the team building that black box. Everything that was previously "just reach for Redis" — where a key lives, what happens when a node dies, what gets evicted when memory fills up — is now the actual subject of the design, and the honest, load-bearing answer to all of it is one integrated decision: consistent hashing to spread keys across shards, leader-follower replication per shard for availability, approximate-LRU eviction under memory pressure, and TTL as a property every entry carries, not a bolt-on feature.

B. Difficulty

Advanced, distributed-systems-heavy — the single-node mechanics (a hash map, an LRU list) are almost trivial; what's hard is doing all of it across a fleet of machines that individually crash, that individually have finite memory, and where a handful of keys can receive wildly more traffic than the rest without warning.

C. What This Problem Tests

D. Requirements

Functional

  1. SET key value [TTL] — store a value under a key, with an optional expiration
  2. GET key — retrieve the value for a key, or a miss if absent or expired
  3. DEL key — explicitly remove a key before its TTL would naturally expire it
  4. An entry set with a TTL is automatically unavailable via GET once that TTL elapses, with no explicit deletion required

Non-Functional

Out of scope: this system is the cache — a downstream database is assumed to exist as the real source of truth (per the cache-aside pattern, Prerequisites D) but is not itself part of this design; multi-key transactions; a full pub/sub messaging layer; rich data structures beyond a string-valued blob (acknowledged as a real extension in AA).

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1If a node crashes, is losing the data it held acceptable (pure cache semantics), or must that data survive the crash?Determines whether this is "purely a fast, disposable cache" or something closer to a low-latency data store with real durability needsDeep Dive 7 (S), Consistency Model (V)
2Can reads be served from a replica, accepting slightly stale data, or must every read reflect the absolute latest write?Directly sets whether followers can absorb read load or exist purely as failover standbyDeep Dive 3 (O), Consistency Model (V)
3Is key access uniformly distributed, or can a small number of keys (a viral post, a celebrity profile) receive disproportionate traffic?A well-balanced hash function does not, by itself, balance traffic — only key countDeep Dive 4 (P)
4What's the expected value size — small tokens/counters, or larger serialized objects?Drives per-node capacity math and how aggressively eviction needs to actCapacity Estimation (G), Deep Dive 6 (R)
5Should clients know the cluster's shard topology directly (a smart client library), or talk to a routing tier that hides it?Changes where the consistent-hashing logic actually lives and who's responsible for keeping it currentAPI Design (I), Deep Dive 2 (N)

F. Assumptions

G. Capacity Estimation

MetricValue / CalculationWhy It Matters
Total dataset sizeIllustratively, 5 TB of hot key-value dataImmediately, obviously larger than any single machine's practical RAM ceiling (tens to low hundreds of GB) — the entire reason this is a distributed-systems problem and not a data-structures one
Usable RAM per node~50 GB usable out of ~64 GB physical, after reserving headroom for fragmentation (R) and process overheadDrives shard count directly: 5 TB / 50 GB ≈ 100 shards' worth of primary data, before replication overhead
Replication factor1 leader + 2 followers per shard (O)Total cluster memory footprint is ~3x the raw dataset size, not 1x — a real and material cost of the availability guarantee
Peak throughputIllustratively, 1M+ operations/second cluster-wide, heavily read-skewedConfirms per-node latency (sub-millisecond) matters far more than per-node raw storage efficiency — this is fundamentally a latency-and-availability problem, not a storage-volume one, the same shape as Rate Limiter (P2) rather than Video Streaming (P7)
Average value size~1 KB (per F's assumption of small-to-medium serialized objects)Confirms the per-node capacity math above and that eviction-list/metadata overhead per entry is non-trivial relative to the value itself (R)

H. Core Entities

EntityPurposeKey Fields
CacheEntryOne stored key-value pair, the atomic unit the whole system exists to servekey, value (opaque bytes), expires_at (M), last_accessed / freq_counter (M)
ShardA slice of the overall keyspace, owned by exactly one leader at a timeshard_id, ring_range (N), leader_node_id, follower_node_ids (O)
NodeOne physical/virtual machine in the cluster, hosting one or more shard rolesnode_id, role (leader/follower per shard), memory_used, maxmemory (R)
RingMembership (shared, gossiped)The current mapping of virtual nodes to physical nodes that every client and node agrees onvirtual_node_id, owning_node_id, version

I. API Design

-- client-facing (via a shard-aware client library, per F):
SET   key value [EX ttl_seconds]   -> OK
GET   key                          -> value | (nil, cache miss)
DEL   key                          -> OK
-- internal, cluster-facing, not client-facing:
(gossip) node heartbeat + ring-membership propagation                (N)
(replication stream) leader -> follower: append-only change stream   (O)
(rebalance) migrate key range from old owner -> new owner on topology change (N)

Why the client library resolves the shard itself instead of asking a router on every call: a routing-tier hop would add a full extra network round trip to every single GET/SET, directly undermining the sub-millisecond latency requirement (D) — instead, the client library keeps a local, gossip-updated copy of the ring (N) and computes the target node itself, the same "push the routing decision as close to the caller as possible" instinct behind Chat System's connection-routing table (Prerequisites K).

J. Basic / Naive High-Level Design (V1)

The simplest thing that satisfies the functional requirement in isolation: one machine holds an in-memory hash map, and every client talks directly to it.

flowchart LR Client1["Client"] --> Node["Single Cache Node
(in-memory hash map)"] Client2["Client"] --> Node Client3["Client"] --> Node

V1 — one node, one hash map. Works exactly until the dataset outgrows one machine's RAM, or that machine dies.

Why this breaks immediately, not eventually: a single node has a hard, fixed memory ceiling — once the dataset (G) exceeds it, there is no more "just add more RAM" available, only "buy a bigger machine," which is not horizontal scaling and hits a wall regardless. Worse, that one node is also a hard single point of failure: it crashes, and every key it held is not just unavailable but gone, taking the entire cache offline for every client at once. The naive fix — run several independent standalone nodes and have clients pick one via hash(key) % N — solves the memory ceiling but not the crash problem, and introduces a new one: adding or removing a single node changes the result of that modulo for nearly every key, forcing almost the entire dataset to relocate (effectively a full cache flush) for one node joining or leaving, the exact failure this document already named in Prerequisites J.

K. Data Model & Store Choice

An in-memory hash table per shard, not a general-purpose database: the access pattern is exclusively "fetch/store/delete by exact key," never a range scan or a query — a plain hash map gives O(1) access, which is the entire point of reaching for a cache instead of a database in the first place. There is no relational structure to preserve and no query planner needed, the same "match the store to the access pattern, not the other way around" principle from Prerequisites B, taken to its simplest possible extreme.

Values are opaque byte blobs to the storage layer itself. Whether a value is a serialized object, a plain string, or a counter is a client-side concern (mirroring Redis's own richer data-type layer, Prerequisites D) — the core storage engine only needs to store and return bytes, plus the small amount of per-entry metadata (M) needed for eviction and TTL bookkeeping.

Optional disk-backed persistence (S) is a durability optimization layered on top of the in-memory store, never the primary store itself — every hot-path read and write still hits memory only; disk exists purely to speed up recovery after a restart, discussed fully in S.

L. End-to-End Flow — One GET Request Routed to Its Shard and Served

sequenceDiagram participant C as Client Library participant Ring as Local Ring Copy (N) participant L as Shard Leader (O) participant Mem as In-Memory Table (K) C->>Ring: hash(key) -> which shard owns this? Ring-->>C: shard leader's node address C->>L: GET key L->>Mem: lookup key alt hit, not expired Mem-->>L: value L->>L: bump recency/frequency (M) L-->>C: value else miss or expired Mem-->>L: not found L-->>C: cache miss Note over C: caller falls back to DB
and SETs the result back (cache-aside, Prerequisites D) end

The client resolves ownership locally before ever making a network call — the ring lookup (N) never leaves the client process on the fast path.

M. Deep Dive 1 — Eviction Policies

Requirement this addresses: bounded, predictable memory per node (D) — once a node's cache is full, adding a new entry means something else has to go, and which entry is the whole question.

PolicyHowTrade-off
Exact LRUA doubly-linked list ordered by recency plus a hash map for O(1) lookup; every single GET moves that entry to the front of the listTextbook-correct, but every read now also does a write (a list-pointer update) — under high concurrency this either serializes on a lock around the list or requires atomic pointer tricks, turning the cheapest, most frequent operation in the whole system into one with real contention cost
Approximate / sampled LRU (chosen default)Track only a lightweight recency timestamp per entry, no strict ordering structure at all; when eviction is needed, randomly sample a small number of keys (e.g. 5) and evict the least-recently-used one among the sample, not globallyNot perfectly precise — a genuinely old key can occasionally survive an eviction round it "should" have lost — but removes the linked-list contention entirely, and converges to behavior statistically close enough to true LRU that the imprecision essentially never matters in practice; this is the real algorithm Redis itself uses
LFU (least frequently used)Track an access-frequency counter per entry (often a small, probabilistic, decaying counter rather than an exact count, to avoid the counter itself becoming a hot piece of contended state) and evict the lowest-frequency entryBetter than LRU for workloads with a stable set of "always popular" keys that shouldn't be evicted just because of one recency dip, but a genuinely new key starts at zero frequency and can get evicted immediately before ever getting a chance to become popular — decay/aging logic exists specifically to counter this
ARC (Adaptive Replacement Cache) / segmented LRUMaintain two lists — one for recently-used-once entries, one for entries used more than once — and dynamically shift the boundary between them based on observed hit patternsAdapts well to mixed workloads that shift between recency-favoring and frequency-favoring access patterns (used in ZFS's and PostgreSQL's buffer caches), at the cost of meaningfully more bookkeeping than a single-list approach

Why exact LRU is the wrong default despite being the "obvious" textbook answer: the whole point of a cache is that its most common operation, by an enormous margin, is a read — paying a write-sized cost (a list mutation, a lock) on every read directly fights the sub-millisecond latency requirement (D) this entire system exists to satisfy. Sampled approximate LRU trades a small, statistically negligible amount of precision for removing that cost entirely, which is exactly the trade real production systems make.

N. Deep Dive 2 — Consistent Hashing for Horizontal Scaling

Requirement this addresses: horizontal scalability (D) and directly fixes V1's fatal rehashing flaw (J) — this is the same mechanism already introduced in Prerequisites J and generalized as its own named pattern in the Patterns Playbook (Patterns L), applied here as the central partitioning decision of the whole problem rather than a supporting detail.

Ring plus virtual nodes: both physical nodes and keys are hashed onto points on a fixed circular ring; a key belongs to whichever node's point is the next one clockwise from it. Each physical node is assigned many virtual-node points scattered around the ring (not just one), which is what actually keeps load balanced — with only one point per node, pure hash luck could give one node a disproportionately large arc, and with it, a disproportionate share of keys.

Rebalancing on node add or remove — the property that makes this the right partitioning choice at all: when a node joins, it claims a set of virtual-node points and only the keys that fall in the small arcs immediately adjacent to those points need to move, migrated in the background directly from their old owner to the new one, while the rest of the ring's keys stay exactly where they were. Removing a node is the mirror image: its arcs are absorbed by their clockwise neighbors, and only those keys relocate. Neither case comes close to the "almost everything moves" failure of naive hash(key) % N (J).

Who keeps the ring up to date: nodes propagate ring-membership changes to each other via a lightweight gossip protocol, and the smart client library (I) holds its own periodically-refreshed copy — the same "everyone eventually agrees on current ownership, without a single bottleneck node being asked on every request" model used by Redis Cluster's cluster bus in practice (AD).

A request that lands on the wrong node during a brief rebalance window is handled with a redirect response pointing the client at the correct current owner (mirroring Redis Cluster's real MOVED/ASK responses) rather than being silently dropped or served incorrectly — a brief, self-correcting inconsistency, not a lasting one.

O. Deep Dive 3 — Replication for High Availability

Requirement this addresses: high availability (D) — consistent hashing (N) solves "which node owns this key" but says nothing about what happens when that one owning node crashes; without replication, losing a node still means losing that entire slice of the keyspace outright.

Leader-follower per shard, the standard model (Prerequisites C), applied at shard granularity rather than to the dataset as a whole: each shard has one leader taking all writes and, by default, all reads, plus one or more followers that continuously receive a stream of changes from it.

Replication ModeHowTrade-off
Asynchronous (chosen default)The leader applies a write locally and acknowledges the client immediately, streaming the change to followers in the background without waiting for their confirmationFastest possible write path — the point of a cache — but a leader crash between "acked the client" and "the change reached a follower" loses that write outright, an acceptable cost given F's pure-cache-semantics assumption
Semi-synchronousThe leader waits for at least one follower to acknowledge receipt before returning success to the clientMeaningfully safer — a leader crash no longer loses the most recent write outright — at the direct cost of added write latency, a real trade-off against this system's core latency requirement (D)

Failover: followers watch the leader's heartbeat; if it goes silent past a bounded threshold, the cluster's coordination layer promotes one follower to leader via the same lease-based election mechanism already used for Job Scheduler's triggering role (Prerequisites F, P10-M) — exactly one new leader is elected, the ring (N) is updated to point at it, and the shard resumes accepting writes, all without any human intervention.

Why followers serve reads rather than sitting idle (per F's assumption): a shard that only ever reads and writes through its leader wastes the follower's capacity entirely; letting followers serve reads spreads read load across the replica set — the same replica-read-scaling reasoning from Prerequisites C — at the direct cost of the staleness window discussed fully in V.

P. Deep Dive 4 — The Hot-Key Problem

Requirement this addresses: even load distribution (D) — and specifically the gap that N alone cannot close, per E's clarifying question 3.

Why a perfectly balanced hash function doesn't prevent this: consistent hashing (N) guarantees that keys are spread evenly across nodes by count — it says nothing at all about the traffic each individual key receives. A single viral key can receive orders of magnitude more requests than its neighbors on the ring, and because it correctly hashes to exactly one shard, that one shard's leader (and its followers, if read traffic is also skewed) absorbs the entire spike alone, no matter how well-distributed every other key is. This is a fundamentally different problem than an unbalanced ring, and "just add more shards" does nothing to fix it — the hot key still lands on exactly one of them.

Why this deserves its own deep dive rather than being folded into N: conflating "balance the keyspace" with "balance the traffic" is a common and understandable mistake — correctly separating them, and having a distinct answer for each, is a clear signal of real production experience with caching systems rather than textbook familiarity with hashing alone.

Q. Deep Dive 5 — Cache Invalidation Strategies

Requirement this addresses: the TTL functional requirement (D) plus the broader question of keeping cached data from silently drifting from its source of truth — famously one of the two genuinely hard problems in computer science.

TTL as a first-class property of every entry, not an optional extra: every CacheEntry (H) carries an expires_at, even if that value is "very far in the future" for effectively-permanent data — treating expiration as the norm rather than the exception is what makes automatic cleanup (M, R) a uniform mechanism instead of a special case bolted on for the entries that happen to have a TTL set.

Write StrategyHowTrade-off
Write-throughA write goes to the cache and the backing database synchronously, before returning successCache and database never disagree, but write latency is the sum of both — directly costly for a system whose entire value proposition is speed
Write-back / write-behindA write lands in the cache immediately and is acknowledged; the database is updated asynchronously afterwardFastest possible write, but a node crash before the async flush completes loses that write from the database's perspective entirely, not just the cache's
Write-around / cache-aside (chosen default, per Prerequisites D)Writes go directly to the database; the cache is populated lazily, only on a subsequent read miss, or invalidated explicitly on writeAvoids ever caching write-heavy, rarely-read data, at the cost of the first read after any write always being a guaranteed miss

Explicit invalidation on write: alongside write-around, a write to the source of truth also issues a DEL for the corresponding cache key, so the next read is a clean miss that repopulates fresh data rather than serving a now-incorrect cached value until its TTL happens to expire on its own.

Thundering herd / cache stampede: when a single hot key's TTL expires, every concurrent request for it misses at the same instant and every one of them independently hammers the database to refetch the same value simultaneously — a self-inflicted traffic spike against the system this cache exists to protect. The fix is request coalescing: the first miss for a given key triggers the actual database fetch and marks that key as "refill in progress"; every other concurrent request for the same key waits on that one in-flight fetch instead of issuing its own, and all of them are satisfied once it completes. Where coalescing isn't practical, a short-lived distributed lock (Prerequisites E) around the refill step achieves the same effect — only the lock holder refetches, and this is exactly the kind of "protect one critical, narrow operation" case Prerequisites E names as the legitimate reason to reach for an external lock at all.

R. Deep Dive 6 — Memory Management & Eviction Under Pressure

Requirement this addresses: bounded, predictable per-node memory (D) — M decided which entry to evict; this decides when eviction actually happens and what happens if it can't keep up.

maxmemory policy, a hard per-node cap: each node is configured with a fixed memory ceiling and a policy for what to do once it's reached — evict something under one of M's policies to make room (the normal, expected behavior), or reject new writes outright with an out-of-memory error rather than evict anything at all, for workloads where "never lose a cached value early" matters more than "always accept new writes" (mirroring Redis's real maxmemory-policy options, including its literal noeviction setting).

Memory fragmentation: as entries of varying sizes are constantly created and freed, the freed gaps left behind don't always line up with the size of the next allocation that needs space, so a node's actually usable memory can be meaningfully less than its physical RAM even while plenty of memory is nominally "free" — a large factor behind G's conservative "usable RAM" estimate, and why allocator choice (e.g. jemalloc over the platform default) is a real, practical lever in production caching systems, not a micro-optimization.

OOM prevention as a design stance, not an afterthought: the system deliberately fails a write explicitly and immediately once truly out of room, rather than letting the underlying process actually exhaust system memory and get killed by the OS — an explicit, recoverable error is a far better failure mode than the whole node (and every key it owns) going down.

Eviction as background work, not purely inline: checking every single key for expiration or over-limit status on every write would add cost to the hottest path in the system; instead, a background reaper thread proactively scans and evicts expired/over-limit entries on its own schedule, while a lightweight lazy check on access (mirroring Redis's active-plus-lazy expiry hybrid) catches anything the background pass hasn't reached yet — most of the cleanup cost is moved off the request path entirely.

S. Deep Dive 7 — Persistence & Recovery

Requirement this addresses: directly revisits E's clarifying question 1 — even granting F's pure-cache-semantics assumption, a cluster that has to rebuild every shard from nothing after every restart pays a real, recurring cost.

ApproachHowTrade-off
RDB-style snapshotPeriodically dump the entire in-memory dataset to disk as a point-in-time fileFast to reload on restart (just read the file back into memory), but loses every write since the last snapshot was taken
AOF-style append logLog every write operation to disk sequentially as it happensFar less data loss on crash (at most the last few unflushed writes), but replaying a long log on restart is slower than loading one snapshot, and the log grows unboundedly without periodic compaction

Why a cache would want any durability at all, given it's not the source of truth: persistence here is a warm-up optimization, not a correctness requirement — the real, current answer for any key still lives in the backing database (F's cache-aside assumption). Without it, a cluster-wide restart (a deploy, a power event) produces a fully cold cache across every shard simultaneously, and every single key becomes a guaranteed miss at once — precisely the thundering-herd scenario named in Q, except triggered by an operational event instead of a single key's TTL, and hitting the database with the traffic of the entire cache's worth of misses at once instead of one key's.

Warm-up strategy, in priority order: if a live, already-warm follower exists for a shard (O), the fastest recovery path is simply promoting or resyncing from it — no disk load needed at all. Only when no live replica exists does a node fall back to reloading from its own RDB snapshot or replaying its AOF log, trading some recovery time for not needing to hit the backing database cold.

T. Full Solution at Scale — Final Architecture

flowchart TB subgraph CLIENTS [" Client Layer "] Lib["Shard-aware client library (I, N)"] end Lib -- "gossip-synced ring copy" --> Ring[("Ring Membership (N)")] subgraph SHARD1 [" Shard 1 "] direction LR L1["Leader"] F1a["Follower"] F1b["Follower"] L1 -- "async/semi-sync replication (O)" --> F1a L1 -- "async/semi-sync replication (O)" --> F1b end subgraph SHARD2 [" Shard 2 "] direction LR L2["Leader"] F2a["Follower"] F2b["Follower"] L2 -- "async/semi-sync replication (O)" --> F2a L2 -- "async/semi-sync replication (O)" --> F2b end Lib -- "GET/SET by hashed key (N)" --> L1 Lib -- "GET/SET by hashed key (N)" --> L2 Lib -- "reads (F, O)" --> F1a Lib -- "reads (F, O)" --> F2a L1 -- "approximate LRU + maxmemory (M, R)" --> Mem1[("In-Memory Table")] L1 -- "snapshot/AOF (S)" --> Disk1[("Optional Disk Backing")]

Everything above is the composition of M through S — no new component introduced here.

U. Design Evolution

VersionWhat ChangedWhy
V1Single in-memory node, no partitioning, no replication (J)Simplest possible thing that satisfies the functional requirement in isolation
V2Multiple standalone nodes with naive hash(key) % N client-side shardingFixes the memory ceiling, but rehashes nearly the entire keyspace on any topology change (J)
V3Consistent hashing with virtual nodes replaces naive modulo shardingOnly keys adjacent to a changed node actually move, fixing V2's core flaw (N)
V4Leader-follower replication added per shardA single node crash no longer means that shard's data is simply gone (O)
V5Exact LRU replaced with sampled approximate LRURemoves lock contention on the hottest operation in the system, at negligible precision cost (M)
V6Hot-key mitigations (local caching, key splitting, dedicated replicas) layered on topConsistent hashing alone balances key count, not key traffic (P)
V7Request coalescing / locking added around cache-miss refillsPrevents a stampede of duplicate refetches against the backing database on a hot key's expiry (Q)
V8Optional RDB/AOF persistence added for warm restartsAvoids a cluster-wide cold-cache stampede after a full restart, without changing the system's correctness model (S)

What stays stable throughout: the tiny GET/SET/DEL contract from I — every later refinement changes how a key is routed, replicated, evicted, or recovered, never the basic shape of what a client calls.

V. Consistency Model

This is, by explicit design (F, O), a system built on eventual/weak consistency between a shard's leader and its followers — a follower's copy of any given key can lag the leader by some small, bounded window (replication lag, Prerequisites C), and a read served from that follower can return a value that's already stale by that same window. Reading from the leader only recovers read-your-own-writes and full strong consistency for that shard, at the direct cost of losing the read-scaling benefit that letting followers serve traffic provides (O) — clients that need to guarantee seeing their own immediately-preceding write (for example, right after a SET) should route that specific follow-up read to the leader, or accept a short client-side "sticky to leader" window after writing, rather than assuming every read anywhere in the cluster reflects every write everywhere. This is the same narrow, deliberate trade of a small strongly-consistent island (the leader, for a brief post-write window) against a much larger eventually-consistent surface (everywhere else, everywhen else) used throughout this document — here, uniquely, applied to the caching layer itself rather than to something the cache merely sits in front of.

W. Failure Scenarios

X. Bottlenecks & Scaling Boundary

StageActual BottleneckFix Applied
V1 single nodeHard RAM ceiling, and a crash takes down the entire cache at onceHorizontal partitioning via consistent hashing across many nodes (N)
Naive hash % N shardingAny topology change relocates nearly the whole keyspaceConsistent hashing with virtual nodes; only adjacent-arc keys move (N)
Unreplicated shardsAny single node crash permanently loses that shard's entire dataLeader-follower replication with automatic failover (O)
Exact LRU under high concurrencyLock contention on the recency list turns every read into a serialization pointSampled approximate LRU removes the shared-structure update entirely (M)
Well-balanced ring, skewed trafficA single hot key overwhelms its one correctly-assigned shardClient-side local caching, key splitting, dedicated hot-key replicas (P)
Synchronized TTL expiry on a hot keyA stampede of simultaneous refetches against the backing databaseRequest coalescing / short-lived lock around the refill (Q)

Y. Design Trade-Offs

DecisionOption AOption BTrade-off
Partitioning schemeNaive hash(key) % NConsistent hashing with virtual nodes (chosen)Modulo sharding is simpler to reason about but relocates nearly everything on any topology change; consistent hashing costs a bit more implementation complexity for vastly cheaper rebalancing
Replication modeSynchronousAsynchronous (chosen default)Synchronous replication never loses an acknowledged write but adds real latency to every write, fighting this system's core purpose; async is faster but can lose the most recent write(s) on a leader crash — acceptable under F's pure-cache assumption
Eviction precisionExact LRUSampled approximate LRU (chosen)Exact LRU is textbook-correct but adds write-sized cost to every read via lock contention; approximate LRU trades negligible precision for removing that cost entirely
Read routingLeader-only readsLeader + follower reads (chosen)Leader-only reads are always fully consistent but waste follower capacity and cap read throughput at one node per shard; follower reads scale throughput at the cost of a bounded staleness window (V)
DurabilityPure in-memory, no persistenceOptional RDB/AOF (chosen)Pure in-memory is simplest and fastest but produces a full cold-cache stampede on every restart; on-disk backing adds write overhead and operational complexity in exchange for materially faster, gentler warm-up

Z. Common Bad Designs

AA. Interviewer Follow-Ups

Follow-UpDesign ChangeComponents ChangedComponents Unchanged
"One key is suddenly getting 100x the traffic of every other key."Directly exercises P's hot-key mitigations — client-side local caching first, key splitting or dedicated replicas if that's not enoughClient library, possibly key-splitting logic at the write pathConsistent hashing, replication, eviction
"Support richer data structures — lists, sets, sorted sets — not just flat string values."Extends the value model at the storage-engine level (K) — the partitioning, replication, and eviction machinery around it is entirely unaffected, since it never inspected value contents to begin withStorage-engine value representationConsistent hashing, replication, eviction policy, TTL mechanics
"A client must always see its own most recent write, even immediately after."Revisits V directly — route that read to the leader (or a short post-write sticky window) rather than a follower, trading some read-scaling benefit for a stronger per-client guaranteeClient read-routing logicReplication topology, eviction, partitioning
"What if a whole availability zone/data center goes down?"Requires cross-AZ replica placement — at least one follower per shard lives outside the primary AZ, so a zone failure still leaves a promotable replica (O) alive elsewhereReplica placement strategyThe replication and failover mechanism itself
"How would you monitor this cluster in production?"Per-shard hit/miss ratio, eviction rate, replication lag, and per-key traffic (to catch hot keys before P becomes an incident) as the core metrics — a natural extension, not a redesignObservability/monitoring layer onlyEverything structural

AB. Interviewer Evaluation Rubric

AC. Interview Simulation

Interviewer: Design a distributed cache — something like Redis Cluster, at scale.
Candidate: The core problem is that one machine's RAM has a hard ceiling, and if it's the only node, it's also a single point of failure — a crash loses everything it held. I'd partition the keyspace across many nodes using consistent hashing rather than a naive hash(key) % N, because the naive version relocates almost every key whenever a node joins or leaves; consistent hashing with virtual nodes only moves the small slice of keys adjacent to the change.
Interviewer: Good. What happens when one of those nodes crashes?
Candidate: That's what replication is for — each shard has a leader and a couple of followers, using standard leader-follower replication. If the leader crashes, a follower gets promoted via leader election, and the ring gets updated to point at it. I'd default to asynchronous replication so writes stay fast, which does mean a crash right after a write can lose that one write — but for a cache backed by a real database as the source of truth, that's an acceptable trade, not a correctness violation.
Interviewer: How do you decide what to evict when a node's memory fills up?
Candidate: Conceptually LRU, but not textbook exact LRU — a real linked list ordered by recency means every single read also has to update that list, which under high concurrency becomes a lock-contention bottleneck on the most frequent operation in the whole system. The practical answer, and what Redis actually does, is sampled approximate LRU: track a lightweight timestamp per key, and when you need to evict, sample a handful of random keys and evict the oldest among just that sample. It's not perfectly precise, but it's close enough in practice and removes the contention entirely.
Interviewer: Suppose one specific key becomes extremely hot — way more traffic than any other key.
Candidate: That's a different problem than an unbalanced hash function — consistent hashing balances how many keys each shard owns, not how much traffic any one key gets. A viral key still lands on exactly one shard no matter how well-distributed everything else is. I'd handle it with a short-TTL local cache directly in the calling clients to absorb repeat reads before they even hit the cluster, and if that's not enough, split the key into several physical copies scattered across different shards by their own hashes, so reads for that one logical key spread across multiple shards instead of hammering one.

AD. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
Redis Cluster (official docs)16,384 fixed hash slots (not a classic unbounded ring) distributed across nodes via CRC16(key) mod 16384; a gossip-based cluster bus propagates slot ownership and node health; each slot's data is replicated to 0+ replicas with quorum-based automatic failoverThe clearest production precedent for nearly every deep dive in this problem at once — partitioning, gossip-based membership, replication, and real approximate-LRU (maxmemory-policy allkeys-lru, sampled by default) and LFU (allkeys-lfu, an 8-bit probabilistic decaying counter) eviction policiesThe fixed 16,384-slot model is a meaningfully different mechanism than a pure hash ring with virtual nodes, though it solves the identical rebalancing problemConfirms N's rebalancing model and M's approximate-eviction reasoning are not interview simplifications but exactly how a real, widely-deployed system works
Memcached (project wiki)An intentionally minimal, single-process slab-allocated LRU cache with no built-in clustering, replication, or persistence at all — sharding is done entirely client-side, typically via a consistent-hashing scheme like Ketama (used in libmemcached)A useful contrast case: proof that a cache can be extremely fast specifically because it deliberately punts partitioning and availability to the client, rather than solving them internallyLosing a node means losing that node's data outright with no automatic recovery — a real production gap this problem's design deliberately closes with OThe slab allocator (fixed-size memory chunks per size class) is a concrete, real answer to R's fragmentation concern, trading some internal memory waste for eliminating fragmentation
AWS ElastiCache (official docs)A managed wrapper around both engines above — for Redis, adds Multi-AZ replication groups with automatic failover and S3-backed snapshot/restore; for Memcached, adds an Auto Discovery client feature specifically because Memcached itself has no native concept of cluster membership to discoverConfirms this problem's replication-plus-failover design (O) and optional-persistence design (S) mirror real managed-service defaults, not an invented interview convenienceBeing a managed wrapper, it doesn't change either underlying engine's fundamental architecture — Memcached still has no native replication even when AWS-managedThe very existence of "Auto Discovery" as an AWS add-on for Memcached is itself confirmation of how real and non-trivial N's ring-membership-propagation problem is in practice

AE. Interview Cheat Sheet

ProblemDistributed Cache
DifficultyAdvanced + Distributed Systems (simple single-node mechanics, hard correctness-and-availability-at-scale)
Core requirementsSub-millisecond GET/SET/DEL with TTL, horizontal scale past one machine's RAM, high availability, even load distribution, bounded per-node memory
Central decisionConsistent hashing for partitioning + leader-follower replication per shard for availability + approximate-LRU eviction + TTL as a first-class entry property
PartitioningRing + virtual nodes; only keys adjacent to a topology change actually migrate, unlike naive hash % N
AvailabilityAsync (default) or semi-sync replication per shard; lease-based leader election on failover, same mechanism as Job Scheduler's leader role
EvictionSampled approximate LRU, not exact LRU — removes lock contention on the hottest operation at negligible precision cost
Hot keysA distinct problem from an unbalanced ring; fixed with client-side local caching, key splitting, or dedicated hot-key replicas — not more shards
InvalidationCache-aside + explicit invalidation on write by default; request coalescing or a short-lived lock to prevent thundering-herd stampedes on expiry
Common mistakesSingle unpartitioned node; naive modulo sharding; exact LRU under real concurrency; assuming a balanced hash function balances traffic; no TTL discipline
What NOT to over-engineerRich data structures, multi-key transactions, and a full pub/sub layer — name them as real extensions (AA), not something to design from scratch here