Engineering Atlas › System Design Interviews

Where to add caches in a design answer

14 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Where to cache

Patterns

Invalidation

Options, roughly in order of preference: a TTL as a safety net on everything; delete the key after a successful database write; drive deletes from change data capture (Debezium reading the Postgres WAL, or DynamoDB Streams), which catches writes from every service; or use versioned keys such as user:42:v17, where a write bumps the version so old entries are simply never read again. Prefer delete over update on write: two concurrent writers that each set the cache can leave the older value in place, while a delete at worst causes one extra miss. Facebook chose delete for the same reason: deletes are idempotent, so retrying one is always safe (Nishtala et al., NSDI 2013). Order matters too: update the database first, then delete. Deleting first leaves a window in which a reader misses, reads the old row and puts it back (Azure Architecture Center).

What interviewers look for

They want a specific placement ("an L1 for the top 1,000 products with a 5-second TTL, Redis for product pages with a 10-minute TTL"), a named pattern, a staleness budget agreed with the requirements, a plan for misses at scale, and an estimate of memory. Staff candidates also say what is not cached and why, such as account balances or inventory counts used for decisions.

A repeatable approach

  1. Find the read-heavy paths and the hot data (often a few percent of keys get most reads).
  2. Choose the layer: CDN for public content, distributed cache for shared data, L1 for very hot or tiny data.
  3. Pick the pattern and the invalidation: cache-aside, TTL, delete on write.
  4. Size it: working set times value size times overhead.
  5. Cover failure: stampede protection, hot keys, cache outage behaviour.

Common mistakes

Down the stack

Layer 1The contract

A cache-aside entry with TTL T and delete-on-write promises that a read returns either the current value or a value at most T old, and usually much fresher. It does not promise read-your-writes across servers with L1 caches. A cache promises nothing about durability: any key may vanish at any moment through eviction, failover or restart, so correctness must never depend on a key being present. Latency is the real contract: a distributed cache hit should be around 0.2 to 1 ms at p99 within a datacenter; anything slower erases the point of caching.

Layer 2The mechanism

Redis evicts according to maxmemory-policy, but out of the box it does not evict at all: maxmemory is 0 (unlimited) on 64-bit builds and the default policy is noeviction, which makes writes fail once memory runs out (Redis docs). Set both before calling it a cache. Its LRU is approximate: on each eviction it samples maxmemory-samples keys (default 5) and evicts the best candidate from a small pool, which avoids keeping a full linked list. allkeys-lfu uses a logarithmic counter per key (0 to 255, saturating around a million hits and decaying once a minute by default), which better protects steadily popular keys from a burst of one-off reads. Memcached stores items in slab classes of fixed chunk sizes and keeps a segmented LRU (hot, warm, cold) per class, so a one-off scan flows through cold and out without evicting active items, so a workload change in value sizes can leave memory stuck in the wrong slab class until the slab rebalancer moves pages. For stampedes, Facebook's Memcache paper describes leases: on a miss the server issues a 64-bit token to one client, at most once every 10 seconds per key, and others are told to wait briefly and retry, while a delete invalidates outstanding tokens so a stale set is rejected. On keys prone to herds, leases cut the peak database query rate from 17K/s to 1.3K/s (NSDI 2013). The XFetch approach refreshes probabilistically before expiry, with probability rising as the TTL nears zero. Redis client-side caching (CLIENT TRACKING, added in Redis 6) pushes invalidation messages to clients that read a key, letting an L1 hold data without a short TTL; if the invalidation connection drops, the client must flush its whole local cache (Redis docs). Real workloads are less read-heavy than textbooks assume: a study of 153 Twitter cache clusters found many far more write-heavy or skewed than expected, and TTLs often decided the working set more than eviction did (Yang et al., OSDI 2020).

Layer 3The machine

A Redis GET of a small value spends a few microseconds in the server; the rest of the 200 to 500 microsecond round trip is the network stack and the hop. Redis runs commands on one main thread, so one instance tops out somewhere around 100k to a few hundred thousand simple operations per second, depending on pipelining, value size and I/O threads. Large values hurt twice: a 1 MB value takes about 0.8 ms just to serialise onto a 10 Gbit link (8 megabits at 10 gigabits per second), and it blocks the event loop for every other client meanwhile. Memory overhead per key is real: a small Redis string key costs perhaps 50 to 100 bytes beyond the payload because of the dict entry, object header and allocator rounding in jemalloc size classes. RDB snapshots fork the process, and copy-on-write can briefly double memory on a write-heavy node.

Why it's built this way

Memory is 10 to 100 times faster than SSD and the network hop is faster than a database query, so keeping hot data in RAM near the app pays. RAM is expensive, so caches hold only the working set and must evict. Exact LRU needs per-access bookkeeping across millions of keys, so production caches approximate it. A cache is a copy, and copies go stale, so every design needs a staleness bound (TTL) and an invalidation path. Because a miss for a hot key multiplies across all clients, systems add leases, coalescing or early refresh to protect the database.

Diagram

sequenceDiagram
    participant A as App server
    participant C as Redis
    participant D as Database
    A->>C: GET product:42
    C-->>A: miss
    A->>D: SELECT product 42
    D-->>A: row
    A->>C: SET product:42 EX 600
    Note over A,D: On update
    A->>D: UPDATE product 42
    A->>C: DEL product:42

Worked example

An e-commerce product page service gets 120k reads per second at peak and 300 product updates per second. There are 50 million products, but about 2 million get 95 percent of views.

Memory: an average cached product is about 4 KB serialised. 2 million times 4 KB is 8 GB; add around 30 percent for key overhead and fragmentation, so about 10.5 GB. Three Redis primaries with about 4 GB of data each, plus a replica each, gives headroom and survives a node loss.

Load: at a 95 percent hit ratio, the database sees 6k reads per second; at 99 percent, 1.2k. A read replica pair handles either, but the difference matters for cost. Each Redis primary sees about 40k operations per second, comfortably within one instance.

Staleness: price changes must appear within a minute. Use cache-aside with a 10-minute TTL as a safety net, and a CDC consumer on the products table that deletes the key within about a second of commit. The CDC path also catches updates from the pricing service, which never calls the product service.

Hot keys: a flash sale product might get 30k reads per second. Add an L1 cache with a 2-second TTL for keys flagged hot, so each of 200 app servers refreshes it at most once per 2 seconds.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Cache-aside + delete on writeSimple, cache failure is safeFirst read after write misses, small race windowDefault for read-heavy data
Write-throughAlways warm, fresh readsEvery write pays twice, caches unread dataRead-soon-after-write data
Write-behindFastest writesData loss if the cache diesCounters, metrics, rebuildable state
L1 in-processMicrosecond reads, absorbs hot keysPer-server staleness, memory per instanceConfig, hot keys, tiny reference data
CDNOffloads origin entirelyPublic content only, purge latencyStatic assets, public pages

Pitfalls in production

Leadership lens

Check yourself

  1. Why delete the cache entry on write instead of updating it?

    Show answer

    Two concurrent writers updating the cache can apply in the opposite order to the database, leaving a stale value until TTL; a delete just forces one fresh read.

  2. Your hit ratio drops from 98 to 92 percent. By how much does database load grow?

    Show answer

    Misses go from 2 to 8 percent of reads, so database reads quadruple.

  3. When is write-behind acceptable?

    Show answer

    When losing the most recent writes on a cache failure is tolerable or recoverable, such as view counters or metrics.

  4. Why does an L1 cache solve a hot key that adding Redis replicas solves less well?

    Show answer

    L1 removes the network hop and spreads the key across every app server's memory, so the shard sees at most one refresh per server per TTL.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →