11. Distributed Cache Advanced + Distributed Systems
A. Realistic Interview Prompt
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
- Recognizing the difference between using a cache (Prerequisites D, and nearly every earlier problem) and building the caching system itself — the ownership, partitioning, and failure-handling concerns that a client of Redis never has to think about, because Redis's own engineers already solved them
- Consistent hashing (Prerequisites J, Patterns Playbook L) as a genuinely load-bearing partitioning mechanism here, not a one-line buzzword — the ring, virtual nodes, and what actually moves when the fleet's shape changes
- Honest reasoning about eviction: recognizing that textbook exact LRU doesn't survive contact with real concurrency and scale, and that every production system's real answer is an approximation
- Replication as an availability mechanism with a real consistency cost — leader-follower per shard, and being explicit about what "eventually consistent between leader and follower" actually means for a cache's correctness guarantees
- Recognizing that a single overloaded key is a fundamentally different failure mode than an unbalanced hash function, and needs a fundamentally different fix
D. Requirements
Functional
SET key value [TTL]— store a value under a key, with an optional expirationGET key— retrieve the value for a key, or a miss if absent or expiredDEL key— explicitly remove a key before its TTL would naturally expire it- An entry set with a TTL is automatically unavailable via
GETonce that TTL elapses, with no explicit deletion required
Non-Functional
- Very low latency — sub-millisecond to low-single-digit-millisecond
GET/SET, the entire reason anything reaches for a cache instead of the database in the first place - Horizontal scalability — a total dataset far larger than any single machine's RAM, served by a fleet of nodes rather than one
- High availability — a node crash should not make its share of keys fully unreachable, and should not require a manual failover
- Even load distribution — both key count and, as far as possible, request traffic spread across the fleet rather than concentrated on a few nodes
- Bounded, predictable memory use per node — a node approaching its memory limit degrades by evicting gracefully, not by crashing or by silently accepting unbounded growth
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
| # | Question | Why It Matters | Design Decision Affected |
|---|---|---|---|
| 1 | If 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 needs | Deep Dive 7 (S), Consistency Model (V) |
| 2 | Can 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 standby | Deep Dive 3 (O), Consistency Model (V) |
| 3 | Is 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 count | Deep Dive 4 (P) |
| 4 | What's the expected value size — small tokens/counters, or larger serialized objects? | Drives per-node capacity math and how aggressively eviction needs to act | Capacity Estimation (G), Deep Dive 6 (R) |
| 5 | Should 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 current | API Design (I), Deep Dive 2 (N) |
F. Assumptions
- Confirmed:
GET/SET/DELwith per-key TTL, sub-millisecond latency target, horizontal scale past one machine's RAM. - Assumption: this is a pure, cache-aside-style cache (E's clarifying question 1) — the source of truth lives elsewhere (a database), so losing a node's data on crash is tolerable in principle, though O and S both discuss reducing how often and how much that actually happens in practice.
- Assumption: followers may serve reads (E's clarifying question 2) — the far more common real-world configuration, and the one that makes replication earn its keep for more than pure failover.
- Assumption: access is skewed, not uniform (E's clarifying question 3) — realistic for almost any real cache workload, and the assumption that makes the hot-key deep dive (P) a first-class part of the design rather than a footnote.
- Assumption: a smart client library maintains shard-ring awareness directly (E's clarifying question 5) — the same model used by real systems (Redis Cluster clients, Memcached's Ketama-based libraries), avoiding a routing-tier hop on every single request.
G. Capacity Estimation
| Metric | Value / Calculation | Why It Matters |
|---|---|---|
| Total dataset size | Illustratively, 5 TB of hot key-value data | Immediately, 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 overhead | Drives shard count directly: 5 TB / 50 GB ≈ 100 shards' worth of primary data, before replication overhead |
| Replication factor | 1 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 throughput | Illustratively, 1M+ operations/second cluster-wide, heavily read-skewed | Confirms 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
| Entity | Purpose | Key Fields |
|---|---|---|
CacheEntry | One stored key-value pair, the atomic unit the whole system exists to serve | key, value (opaque bytes), expires_at (M), last_accessed / freq_counter (M) |
Shard | A slice of the overall keyspace, owned by exactly one leader at a time | shard_id, ring_range (N), leader_node_id, follower_node_ids (O) |
Node | One physical/virtual machine in the cluster, hosting one or more shard roles | node_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 on | virtual_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.
(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
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.
| Policy | How | Trade-off |
|---|---|---|
| Exact LRU | A 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 list | Textbook-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 globally | Not 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 entry | Better 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 LRU | Maintain 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 patterns | Adapts 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 Mode | How | Trade-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 confirmation | Fastest 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-synchronous | The leader waits for at least one follower to acknowledge receipt before returning success to the client | Meaningfully 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.
- Client-side local (L1) caching of known-hot keys, with a short TTL — a thin, in-process cache directly inside each calling client absorbs a large share of repeat reads for a hot key before they ever reach the cluster at all, trading a small amount of extra staleness for removing the request from the network path entirely.
- Key splitting — store a hot logical key's value under several derived physical keys (
key#0,key#1, ...key#N) that consistent hashing naturally scatters across different shards, and have clients pick one at random or round-robin on read; a write now has to fan out to all the split copies, a real added cost, but read traffic for that one logical key is spread across several shards instead of concentrated on one. - Dedicated extra read replicas for specifically identified or predicted hot keys — rather than adding replicas uniformly across the whole cluster, add them only where monitoring (or predictable patterns, like a known upcoming product launch) shows the need, scaling read capacity for that one shard independently of the rest of the fleet.
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 Strategy | How | Trade-off |
|---|---|---|
| Write-through | A write goes to the cache and the backing database synchronously, before returning success | Cache 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-behind | A write lands in the cache immediately and is acknowledged; the database is updated asynchronously afterward | Fastest 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 write | Avoids 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.
| Approach | How | Trade-off |
|---|---|---|
| RDB-style snapshot | Periodically dump the entire in-memory dataset to disk as a point-in-time file | Fast to reload on restart (just read the file back into memory), but loses every write since the last snapshot was taken |
| AOF-style append log | Log every write operation to disk sequentially as it happens | Far 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
Everything above is the composition of M through S — no new component introduced here.
U. Design Evolution
| Version | What Changed | Why |
|---|---|---|
| V1 | Single in-memory node, no partitioning, no replication (J) | Simplest possible thing that satisfies the functional requirement in isolation |
| V2 | Multiple standalone nodes with naive hash(key) % N client-side sharding | Fixes the memory ceiling, but rehashes nearly the entire keyspace on any topology change (J) |
| V3 | Consistent hashing with virtual nodes replaces naive modulo sharding | Only keys adjacent to a changed node actually move, fixing V2's core flaw (N) |
| V4 | Leader-follower replication added per shard | A single node crash no longer means that shard's data is simply gone (O) |
| V5 | Exact LRU replaced with sampled approximate LRU | Removes lock contention on the hottest operation in the system, at negligible precision cost (M) |
| V6 | Hot-key mitigations (local caching, key splitting, dedicated replicas) layered on top | Consistent hashing alone balances key count, not key traffic (P) |
| V7 | Request coalescing / locking added around cache-miss refills | Prevents a stampede of duplicate refetches against the backing database on a hot key's expiry (Q) |
| V8 | Optional RDB/AOF persistence added for warm restarts | Avoids 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
- A shard's leader crashes: its heartbeat goes silent, a follower is promoted via lease-based election (O), and the ring (N) is updated to point at the new leader — writes since the last replicated update may be lost (per the async-replication trade-off), but the shard itself becomes available again within one election cycle, not permanently unavailable.
- A follower crashes: no impact on writes at all; read capacity for that shard temporarily drops to whatever remains, and the follower resyncs from its leader once it recovers.
- A node is added or removed from the cluster: only the keys in the ring arcs immediately adjacent to the change actually migrate (N); everything else on the ring is entirely unaffected.
- A single key becomes disproportionately hot: the shard owning it absorbs elevated load regardless of how well-balanced the rest of the ring is, until one of P's mitigations (local caching, key splitting, dedicated replicas) is applied.
- A node approaches its memory limit: eviction (M, R) proactively frees space under the configured policy; if configured for
noevictioninstead, new writes are rejected outright with an explicit error rather than the process crashing from real OOM. - The whole cluster restarts (a deploy, a power event): without persistence, every shard comes back fully cold and the backing database absorbs the resulting miss storm; with RDB/AOF (S), each node warms up from disk (or, faster, resyncs from an already-warm follower) instead.
X. Bottlenecks & Scaling Boundary
| Stage | Actual Bottleneck | Fix Applied |
|---|---|---|
| V1 single node | Hard RAM ceiling, and a crash takes down the entire cache at once | Horizontal partitioning via consistent hashing across many nodes (N) |
Naive hash % N sharding | Any topology change relocates nearly the whole keyspace | Consistent hashing with virtual nodes; only adjacent-arc keys move (N) |
| Unreplicated shards | Any single node crash permanently loses that shard's entire data | Leader-follower replication with automatic failover (O) |
| Exact LRU under high concurrency | Lock contention on the recency list turns every read into a serialization point | Sampled approximate LRU removes the shared-structure update entirely (M) |
| Well-balanced ring, skewed traffic | A single hot key overwhelms its one correctly-assigned shard | Client-side local caching, key splitting, dedicated hot-key replicas (P) |
| Synchronized TTL expiry on a hot key | A stampede of simultaneous refetches against the backing database | Request coalescing / short-lived lock around the refill (Q) |
Y. Design Trade-Offs
| Decision | Option A | Option B | Trade-off |
|---|---|---|---|
| Partitioning scheme | Naive hash(key) % N | Consistent 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 mode | Synchronous | Asynchronous (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 precision | Exact LRU | Sampled 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 routing | Leader-only reads | Leader + 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) |
| Durability | Pure in-memory, no persistence | Optional 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
- A single, unpartitioned cache node with no replication. A hard memory ceiling and a hard single point of failure at once — the most important mistake to avoid on this problem.
- Naive
hash(key) % Nclient-side sharding. "Solves" the memory ceiling while quietly reintroducing a full-keyspace-reshuffle problem on every scaling event. - Exact, textbook LRU implemented with a real shared linked list under concurrent access. Turns the system's most frequent, most latency-sensitive operation into a lock-contended one, directly undermining the core latency requirement.
- Treating a well-balanced hash ring as sufficient protection against hot keys. Confuses balancing key count with balancing key traffic — two genuinely different problems with two genuinely different fixes (P).
- No TTL story at all, or TTL bolted on as an afterthought rather than a property every entry carries. Leads to unbounded memory growth from data that should have expired long ago, and ad hoc, inconsistent cleanup logic.
AA. Interviewer Follow-Ups
| Follow-Up | Design Change | Components Changed | Components 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 enough | Client library, possibly key-splitting logic at the write path | Consistent 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 with | Storage-engine value representation | Consistent 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 guarantee | Client read-routing logic | Replication 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 elsewhere | Replica placement strategy | The 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 redesign | Observability/monitoring layer only | Everything structural |
AB. Interviewer Evaluation Rubric
- Weak candidate: proposes a single cache node or naive
hash % Nsharding with no discussion of what happens on a topology change or a node crash; treats "use LRU" as a complete answer with no acknowledgment of what that actually costs at scale. - Mid-level: correctly reaches for consistent hashing and leader-follower replication, but doesn't reason about what happens to reads/writes during a rebalance or failover, and hasn't considered that a well-balanced ring says nothing about balanced traffic.
- Senior: proactively distinguishes exact LRU from approximate/sampled LRU and explains why the approximation is the right production choice; treats TTL as a first-class property of every entry; is explicit about the eventual-consistency window between leader and followers rather than glossing over it.
- Staff+: raises the hot-key problem unprompted as distinct from load balancing generally; reasons concretely about cache-stampede/thundering-herd mitigation via request coalescing; discusses persistence as a warm-up optimization rather than a correctness requirement, and can explain precisely why it isn't the latter.
AC. Interview Simulation
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.AD. Online Solution Comparison
| Source | Approach | Strength | Weakness | Worth 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 failover | The 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 policies | The fixed 16,384-slot model is a meaningfully different mechanism than a pure hash ring with virtual nodes, though it solves the identical rebalancing problem | Confirms 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 internally | Losing a node means losing that node's data outright with no automatic recovery — a real production gap this problem's design deliberately closes with O | The 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 discover | Confirms this problem's replication-plus-failover design (O) and optional-persistence design (S) mirror real managed-service defaults, not an invented interview convenience | Being a managed wrapper, it doesn't change either underlying engine's fundamental architecture — Memcached still has no native replication even when AWS-managed | The 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
| Problem | Distributed Cache |
| Difficulty | Advanced + Distributed Systems (simple single-node mechanics, hard correctness-and-availability-at-scale) |
| Core requirements | Sub-millisecond GET/SET/DEL with TTL, horizontal scale past one machine's RAM, high availability, even load distribution, bounded per-node memory |
| Central decision | Consistent hashing for partitioning + leader-follower replication per shard for availability + approximate-LRU eviction + TTL as a first-class entry property |
| Partitioning | Ring + virtual nodes; only keys adjacent to a topology change actually migrate, unlike naive hash % N |
| Availability | Async (default) or semi-sync replication per shard; lease-based leader election on failover, same mechanism as Job Scheduler's leader role |
| Eviction | Sampled approximate LRU, not exact LRU — removes lock contention on the hottest operation at negligible precision cost |
| Hot keys | A distinct problem from an unbalanced ring; fixed with client-side local caching, key splitting, or dedicated hot-key replicas — not more shards |
| Invalidation | Cache-aside + explicit invalidation on write by default; request coalescing or a short-lived lock to prevent thundering-herd stampedes on expiry |
| Common mistakes | Single unpartitioned node; naive modulo sharding; exact LRU under real concurrency; assuming a balanced hash function balances traffic; no TTL discipline |
| What NOT to over-engineer | Rich data structures, multi-key transactions, and a full pub/sub layer — name them as real extensions (AA), not something to design from scratch here |