Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
- 12Relational Databases and Data Modeling for System Design
- 13Database Indexes and Query Performance: How to Make Reads Fast
- 14Transactions, Isolation Levels and Concurrency Control: Preventing Race Conditions
- 15Database Replication: Primary-Replica, Read Replicas, Lag and Failover
- 16Database Sharding and Partitioning: How It Works and When You Need It
- 17SQL vs NoSQL: Data Models and How to Choose the Right Database
6 · Distributed Systems
- 18Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks
- 19CAP Theorem and Consistency Models Explained Without the Myths
- 20Idempotency, Retries and Message Delivery Guarantees
- 21Distributed Transactions: Two-Phase Commit, the Outbox Pattern and Sagas
- 22Distributed Locks, Leases, Leader Election and Consensus
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
- 33Design a Notification System: Email, SMS and Push at Scale
- 34Design a Chat System Like a Messaging App
- 35Design a News Feed: Fan-Out, Ranking and the Celebrity Problem
- 36Design Search Autocomplete (Typeahead)
- 37Design a Distributed Job Scheduler
- 38Design a Ticket Booking System Without Overselling
- 39Design a Payment System: Idempotency, Ledgers and Reconciliation
11 · Large-Scale Case Studies
- 40Design a Video Streaming Platform: Upload, Transcoding and Adaptive Streaming
- 41Design a Ride-Hailing System: Location, Matching and Real-Time Updates
- 42Design a Food Delivery Platform: Orders, Restaurants and Delivery Partners
- 43Design an E-Commerce System: Catalog, Cart, Checkout, Inventory and Flash Sales
- 44Design a Web Search Engine: Crawling, Inverted Indexes and Ranking
12 · Interview Readiness
Lesson 32 · Interview Method and Foundation Case Studies
Design a Distributed Cache
Design an in-memory cache service shared by many applications: requirements and estimates, the get/set/delete API, a single-node design with TTLs and LRU eviction, then partitioning with consistent hashing or hash slots, client-side versus proxy routing, replication and failover, rebalancing, hot keys, cache stampedes, and what consistency a cache can and cannot promise.
For
SDE-1 and SDE-2 candidates asked to design a cache or key-value store, and engineers who run Redis or Memcached clusters and want to understand their design choices.
Level
Before you start
Read Caching (article 09), Database Replication (article 15) and Sharding and Partitioning (article 16), including consistent hashing. The interview method is in article 29.
On this page
Key takeaways
- A distributed cache spreads keys across many in-memory nodes so total memory and throughput grow by adding nodes, and replicates each partition so a node loss does not empty part of the cache.
- Use consistent hashing with virtual nodes or a fixed set of hash slots, so adding or removing nodes moves only a small share of keys.
- Routing can live in a smart client, a proxy tier or the servers themselves; each trades latency, simplicity and client complexity.
- A cache is not a database: it may lose data on failure, serve slightly stale data and evict anything, so callers must always be able to fall back to the source of truth.
- The operational dangers are cold-cache stampedes after failures, hot keys and stale cluster maps, so design for them explicitly.
Several teams at a company each run their own Redis instance, sized by guesswork. One team's cache fills up and starts evicting hot data; another team's is 90% empty. When a cache machine dies, its team's service floods the database. The platform team is asked to build a shared, distributed cache service: one system, many tenants, capacity that grows by adding machines, and no single machine whose loss takes out a big slice of the cache.
This case study designs that service. It is also how you would answer "design a key-value store" or "design Memcached" in an interview, with the important difference that a cache may lose data, which shapes almost every decision.
What you will learn
#- How to scope a cache service and estimate its memory, throughput and node count.
- The design of a single cache node: hash table, TTL expiry and LRU eviction.
- Partitioning with consistent hashing or hash slots, and routing requests.
- Replication, failure detection and failover for a cache.
- Rebalancing when nodes are added or removed.
- Hot keys, stampedes and cold caches.
- What consistency a cache can promise.
1. The problem
#Build an in-memory cache service that many internal services use to store and retrieve values by key with low latency. It should scale horizontally, survive node failures without losing large parts of the cache, isolate tenants from each other, and be simple for application teams to use.
2. Clarify the requirements
#Question | Assumed answer | Impact |
|---|---|---|
Cache or database? | Cache: data can always be recomputed or reloaded from a source of truth | No strong durability; simpler replication |
Operations? |
| API and data structures |
Value sizes? | Mostly under 10 KB, maximum 1 MB | Memory layout, network |
Throughput and latency? | About 1 million operations per second at peak, 80% reads; p99 under 2 ms within a zone | Node count, client design |
Total data? | About 1.5 TB of hot data across tenants, growing | Node count |
Availability? | A node loss must not drop a large part of the cache | Replication |
Multi-tenant? | Yes, about 30 teams | Quotas, isolation, key prefixes |
Persistence across restarts? | Nice to have, not required | Optional snapshotting |
Cross-region? | Each region has its own cache | No cross-region replication |
Functional requirements
get(key),set(key, value, ttl),delete(key),multi_get(keys).incr(key, delta)andcompare_and_set(key, expected_version, value)for safe concurrent updates.- Per-tenant namespaces and memory quotas.
- Automatic eviction when memory is full.
Out of scope: cross-region replication, durable storage guarantees, complex data structures beyond counters (a real product such as Redis offers many more).
Non-functional requirements
Quality | Target |
|---|---|
Latency | p99 under 2 ms for |
Throughput | 1 million operations per second at peak, scalable by adding nodes |
Availability | 99.95%; losing one node affects at most a small share of keys briefly |
Durability | Not required; losing data on failure is acceptable but should be limited |
Isolation | One tenant cannot evict or slow another beyond its quota |
3. Assumptions and scale estimation
#Data: 1.5 TB of hot data (values plus keys plus metadata overhead)Usable memory per node (assumed instance size, after leaving room for the OS, fragmentation and replication buffers): 64 GBPrimary nodes for data: 1.5 TB / 64 GB = about 24 nodesWith one replica each: 48 nodesThroughput: 1,000,000 ops/s / 24 primaries = about 42,000 ops/s per primary (if replicas also serve reads, lower per node)Network: 1,000,000 ops/s x 1 KB average = about 1 GB/s = 8 Gbps aggregate -> about 0.33 Gbps per primary, well within typical NICsConclusions: memory, not throughput, decides the node count (each node is lightly loaded at about 42,000 operations per second, an assumption to confirm with load tests). We need partitioning (no machine holds 1.5 TB in memory) and replication (so a node loss does not drop a 1/24 slice of the cache).
4. API design
#Clients use a library that speaks a compact protocol over persistent TCP connections (an HTTP API would add too much overhead per operation at this latency target):
get(namespace, key) -> value, version | MISSmulti_get(namespace, [keys]) -> {key: value | MISS}set(namespace, key, value, ttl_seconds) -> OK | ERR_TOO_LARGE | ERR_QUOTAdelete(namespace, key) -> OKincr(namespace, key, delta, ttl_seconds) -> new_valuecompare_and_set(namespace, key, expected_version, value, ttl) -> OK | VERSION_MISMATCH- Namespaces prefix every key (
feed:,profile:), map to tenants and carry quotas. - Versions support optimistic concurrency (article 14) and can help with the stale-set race described in article 09.
- Every set has a TTL (the library enforces a maximum default), so mistakes heal themselves.
5. Data model
#Each node keeps, in memory:
Hash table: key -> entryentry = { value bytes, expires_at, for TTL version, for compare-and-set LRU links (prev/next) or an access clock, for eviction}- Expiry: checked lazily on access (an expired key is treated as a miss and removed) and actively by a background task that samples keys with TTLs and removes expired ones, so memory is reclaimed even for keys nobody reads.
- Eviction: when memory reaches the node's limit, evict by an LRU or LFU policy. Exact LRU needs a linked list touched on every read; Redis, for example, uses an approximated LRU that samples a few keys and evicts the oldest among them, because it costs much less memory and is close to exact in practice (Key eviction).
- Memory management: many small allocations fragment memory; cache servers typically use allocators or slab-style pools that group values by size. Plan for some overhead beyond raw data size (our 64 GB "usable" figure already assumes this).
- Per-namespace accounting tracks memory per tenant to enforce quotas: when a tenant exceeds its quota, eviction happens within that tenant's keys first.
6. Baseline architecture
#App servers (clients) ---> one cache server (hash table, TTL, LRU)A single node is simple and fast, and fine for small workloads. But it holds at most one machine's memory, its throughput is one machine's, and when it fails, every client experiences 100% misses at once.
7. What breaks first?
#- Memory: 1.5 TB does not fit on one node.
- Availability: one node is a single point of failure, and its failure causes a stampede on the databases behind the cache.
- Partitioning: with many nodes, how does a client know which node holds a key, and what happens when nodes are added?
- Hot keys: one very popular key can overload its node.
- Cold starts: after a failure or restart, a node's keys must be refilled, loading the source of truth.
8. Scaling the design
#Partitioning keys
#Spread keys across nodes by hashing. As article 16 showed, hash(key) mod N moves almost every key when N changes, which empties the cache in effect. Two better options:
- Consistent hashing with virtual nodes: each node owns many points on a hash ring; adding a node moves only about 1/N of keys, spread evenly. Amazon's Dynamo paper describes this technique (Dynamo).
- A fixed number of hash slots: hash each key into one of a fixed set of slots, and assign slots to nodes in a small table. Redis Cluster uses 16,384 slots, mapping a key with a CRC16 hash modulo 16,384, and lets clients force related keys into the same slot with "hash tags" (a part of the key in braces) so multi-key operations work (Redis cluster specification). Moving capacity means moving whole slots.
Choice: hash slots (for example 16,384), which make rebalancing a matter of reassigning slots and are easy to reason about.
Routing requests
#Approach | How | Pros | Cons |
|---|---|---|---|
Smart client | The client library holds the slot-to-node map and sends each request directly to the right node | No extra hop; lowest latency | Every language needs a capable client; map updates must reach all clients |
Proxy tier | Clients talk to stateless proxies that route to nodes | Simple clients; central control | An extra network hop; proxies must scale and stay available |
Server redirects | Any node accepts a request and, if it does not own the key, replies with the correct node (clients learn and cache the map) | Self-correcting maps | Extra round trips when maps are stale |
Redis Cluster combines the first and third: clients cache the map, and nodes reply with redirection responses when a key lives elsewhere, which also handles keys that are mid-migration (Redis cluster specification).
Choice: smart clients with server redirects as the safety net; a proxy tier for languages without a good client.
Replication and failover
#Each slot range has a primary and at least one replica in a different availability zone.
- Replication is asynchronous (writes are acknowledged by the primary immediately), which keeps latency low; Redis's documentation describes its default replication as asynchronous (Redis replication). The cost: a failover can lose the last few writes, which is acceptable for a cache.
- Failure detection: nodes exchange heartbeats (Redis Cluster uses a gossip-based cluster bus), or a separate coordination service (etcd or ZooKeeper, article 22) tracks membership. A primary is declared failed only when a majority agrees, to avoid splitting the cluster on a single node's network blip.
- Promotion: a replica of the failed primary is promoted and the slot map updated; clients learn the change through redirects or map refreshes.
- Read replicas (optional): serving reads from replicas adds read capacity but returns slightly stale data. Offer it per namespace, as an opt-in.
Rebalancing
#When adding nodes:
- Choose slots to move to the new node (for an even spread).
- For each slot, copy keys from the old owner to the new one while the slot keeps serving.
- During the move, reads for a key not yet copied are answered by the old owner (or redirected), writes go to the new owner.
- Switch ownership in the map; clients update.
Because it is a cache, an acceptable simpler alternative is to move the slot without copying and let the new owner warm up through misses, if the source of truth can absorb the extra load. Move slots gradually either way.
Hot keys
#A single key can receive more traffic than one node can serve (a celebrity profile, a flash-sale product, article 09):
- Client-side local caching of the hottest keys for a second or two (the largest win).
- Replicating hot keys to several nodes under suffixed keys, read at random.
- Reads from replicas for those keys.
- Detect hot keys by sampling access counts per key on each node and reporting the top ones.
Stampedes and cold caches
#When a node fails or a popular key expires, many clients miss at once and hit the source of truth (article 09):
- Replicas make node failures mostly invisible (only the last few writes are lost).
- Request coalescing in the client library: one load per key per client at a time.
- TTL jitter to avoid synchronised expiry.
- Optional persistence (snapshots or an append-only file) lets a restarted node reload its data instead of starting empty; Redis documents both approaches and their trade-offs (Redis persistence).
- Gradual warm-up: bring replacement capacity in slowly.
Multi-get and connection efficiency
#A multi_get of 100 keys spans many nodes. The client groups keys by node, sends one request per node in parallel, and merges results; latency is that of the slowest node involved. Keys that are always read together can share a hash tag so they live on one node. Clients keep persistent connection pools and pipeline requests (send several without waiting for each reply) to keep per-operation overhead low.
The evolved architecture
#App servers (smart client: slot map cache, local hot-key cache, coalescing, pipelining) | direct to owning primary (redirect if the map is stale) v+-------------------- 16,384 hash slots over 24 shards --------------------+| Shard 1: primary (zone A) -> replica (zone B) || Shard 2: primary (zone B) -> replica (zone C) ... || Shard 24: primary (zone C) -> replica (zone A) |+----------------------------------------------------------------------------+ ^ membership, failure detection and slot map: cluster gossip or a consensus storeOperations: per-tenant quotas, metrics, hot-key reports, rebalancing tool9. Failure scenarios and reliability
#Failure | Effect | Response |
|---|---|---|
Primary node crashes | Its slots are briefly unavailable | Majority-confirmed failure; replica promoted within seconds; last few writes lost (acceptable) |
Primary and replica both lost | Those slots are empty | Remaining nodes take over the slots; clients refill from the source of truth with coalescing; persistence helps if available |
Network partition isolates a primary | It may keep accepting writes from clients on its side | Cluster on the majority side promotes a replica; the isolated primary stops accepting writes after it loses contact with the majority for a timeout (Redis Cluster behaves similarly) |
Client has a stale slot map | Requests go to the wrong node | Redirect responses; client refreshes its map |
Memory full | Evictions rise; hit ratio falls | Per-tenant quotas, alerting on eviction rate, add nodes |
Hot key | One node saturated | Client local cache, key replication |
Mass expiry | Source of truth overloaded | TTL jitter, coalescing, early refresh |
A tenant floods the cache | Other tenants' keys evicted | Per-tenant quotas, per-tenant rate limits |
10. Consistency
#A cache makes weak promises, and callers must be designed for them:
- Data may disappear at any time (eviction, failover, restart). Every read must be able to fall back to the source of truth.
- Data may be stale relative to the source of truth. Callers choose TTLs and invalidation (article 09), and the stale-set race applies;
compare_and_setwith versions helps. - Writes may be lost on failover (asynchronous replication). Never use the cache as the only copy of important data.
- Within one key on its primary, operations are atomic and ordered:
incrandcompare_and_setare safe for counters and optimistic updates. - Reads from replicas are eventually consistent; opt in only where that is acceptable.
The Facebook memcache paper is a good example of these trade-offs at very large scale: it describes favouring performance and availability over strict consistency, with mechanisms such as leases to reduce stale sets and thundering herds (Scaling Memcache at Facebook).
11. Security
#- Private network only; the cache is never reachable from the internet.
- Authentication and per-tenant access control (for example per-namespace credentials or access-control lists), so one team cannot read another's keys.
- TLS for traffic if sensitive data is cached, or a policy of not caching sensitive data at all (article 09).
- Quotas and rate limits per tenant, against accidental floods.
- No secrets in keys (keys appear in logs and metrics).
12. Observability
#Signal | Why |
|---|---|
Hit ratio per namespace | The primary measure of usefulness; a drop predicts load on databases |
Latency p50 and p99 per node | Spot sick nodes |
Operations per second and CPU per node | Capacity and hot shards |
Memory used, evictions and expirations per namespace | Quota tuning, sizing |
Hot-key reports | Proactive mitigation |
Replication lag, failovers, slot migrations | Cluster health |
Client-side redirects and map refreshes | Stale maps, ongoing rebalancing |
13. Trade-offs
#Decision | Chosen | Alternative | Why |
|---|---|---|---|
Partitioning | Fixed hash slots | Consistent hashing ring | Simple rebalancing and reasoning; both move few keys |
Routing | Smart client plus redirects | Proxy tier | Lowest latency; proxy offered where clients are weak |
Replication | Asynchronous, one replica per shard in another zone | Synchronous | Latency matters more than the last few writes |
Reads | Primary by default, replicas opt-in | Always replicas | Freshness by default |
Eviction | Approximated LRU per tenant quota | Exact LRU | Lower memory and CPU cost, nearly the same result |
Persistence | Optional snapshots | None | Faster recovery for tenants who need it |
14. The final interview answer
#"This is a multi-tenant, in-memory cache for about 1.5 TB of hot data and a million operations a second, with p99 under 2 ms in a zone. Memory decides the size: about 24 primary shards of 64 GB, each with an asynchronous replica in another zone.
Keys are hashed into 16,384 slots assigned to shards. Smart clients cache the slot map and talk directly to primaries; a node that receives a key it doesn't own redirects the client. Each node is a hash table with lazy and active TTL expiry and approximated LRU eviction within per-tenant quotas.
Failure detection needs a majority, and a replica is promoted within seconds; we accept losing the last few writes because this is a cache. Adding nodes moves slots gradually. Hot keys are absorbed by short-lived client-side caches and key replication, and stampedes by request coalescing and TTL jitter.
Callers must treat the cache as optional and possibly stale: every read can fall back to the source of truth, and nothing important lives only in the cache."
Exercises
#Exercise 1
Size the cluster
#A team needs to cache 400 GB of data with 300,000 operations per second at peak. Nodes offer 48 GB usable memory and are load-tested at 60,000 operations per second each. How many primaries and replicas do you need? Which constraint decides?
Exercise 2: Losing a shard
#In the 24-shard design, a whole shard (primary and replica) is lost because both were accidentally placed on the same physical host. Estimate the extra load on databases if the cache served 800,000 reads per second at a 95% hit ratio before the failure. What design rule would have prevented this?
Exercise 3: Multi-get across shards
#A page needs 200 keys from the cache. Describe how the client should fetch them, what determines latency, and two ways to make it faster.
Exercise 4: Cache or database?
#A team wants to store user shopping carts only in the cache "because it is fast and carts are temporary". What can go wrong, and what would you recommend?
Show solutionHide solution
Memory: 400 GB / 48 GB = 8.3 -> 9 primariesThroughput: 300,000 / 60,000 = 5 primariesMemory decides: 9 primaries, plus 9 replicas (in different zones) = 18 nodes. At 9 primaries, each handles about 33,000 operations per second, comfortably under the tested 60,000, leaving room for skew and growth. Add a little memory headroom (data grows, and evictions start when nodes are full), so 10 primaries would be sensible.
Exercise 2
Show solutionHide solution
One shard holds about 1/24 of keys, and assuming traffic spreads evenly, about 1/24 of reads: 800,000 / 24 = about 33,000 reads per second for that shard's keys. Before the failure, 95% of them were hits, so the database saw about 1,700 per second for those keys. After the loss, all 33,000 miss (until the keys are reloaded into the shard's new owners), so database reads for those keys rise from about 1,700 to about 33,000 per second, a roughly 20-fold jump. Across the whole cache, database reads go from about 40,000 per second (5% of 800,000) to about 71,000 per second, nearly double, and the extra load is concentrated on whichever tables those keys came from. With request coalescing and warm-up the spike shortens, but the databases may still need load shedding.
Prevention: anti-affinity placement rules: a primary and its replicas must be on different hosts and different availability zones, enforced by the orchestration system and checked continuously.
Exercise 3
Show solutionHide solution
The client groups the 200 keys by owning node (using the slot map), sends one multi-key request per node in parallel over existing connections, and merges the results; misses are loaded from the source of truth (in parallel, with coalescing) and written back.
Latency is determined by the slowest node involved (tail latency, article 02): more nodes touched means a higher chance one is slow.
Faster: (1) co-locate keys that are read together with a shared hash tag (for example all of one user's page fragments), so the request touches one or a few nodes; (2) cache the assembled page (or a larger fragment) as one key, at the cost of invalidation complexity; (3) a short client-side local cache for the most common keys; (4) hedged requests to replicas for the slowest nodes, if replicas are allowed for that data.
Exercise 4
Show solutionHide solution
Risks: carts disappear on eviction (memory pressure from any tenant within the shared quota), on failover (asynchronous replication loses recent writes), on restarts without persistence, and on mistakes such as a mass flush. Users lose carts at checkout time, which costs real revenue.
Recommendation: keep carts in a durable store (a relational table or a key-value database with replication), and optionally cache them for fast reads. If the team wants the speed of an in-memory store as the primary store, use one configured as a database: persistence enabled, replication with failover, no eviction (noeviction-style policy), backups and its own capacity, separate from the shared cache. The key rule: the shared cache must only hold data that can be recomputed or reloaded.
Where to go next
#- Next: Design a Notification System: Email, SMS and Push at Scale, the first Level 2 case study.
- Related: article 09 (caching patterns and stampedes) and article 16 (consistent hashing in code).
Sources & further reading
- Technology
- Software Engineering
- #caching
- #case study
- #distributed systems
- #roadmap
- #scalability
- #system design
- #system design interview
Connected lessons
Linked from 2 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
Start Here: Your Roadmap from Zero to Job-Ready DSA
Guide · Technology
Heaps and Priority Queues
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

