The fastest way to stall in a system design round is to say “we’ll put a cache in front of it” and stop there. The interviewer’s next question is always some version of this: who writes to the cache, how does it go stale, and what happens the moment a hot key expires under traffic. Answer those with a named pattern and its failure mode and you sound like you’ve run a cache in production. Wave at them and you don’t.
Caching decisions split along two axes that people constantly blur together. Read strategies control how data gets into the cache. Write strategies control how updates reach the backing store and how fresh the cache stays. You choose one of each, and the interesting tradeoffs live in the write column.
Cache-aside versus read-through
Cache-aside, sometimes called lazy loading, keeps the application in control. A read checks the cache first; on a miss, the app queries the database, stores the result in the cache, and returns it. The cache itself never reaches back to the database. This is the right default for nearly every interview scenario. It only caches keys that were actually requested, and if the cache goes down, reads fall through to a slower database instead of failing outright.
Its weaknesses are the cold-start miss and the repeated glue code. Every read path has to implement the same check-and-backfill sequence, and the first request for any key eats full database latency. Read-through hides that by letting the cache library load from the database on a miss, so the application just asks the cache and gets an answer. It reads cleaner but binds you to a cache that supports the pattern, and plenty of teams approximate it with a thin wrapper around cache-aside. From the database’s side the two behave almost identically, so treat read-through as the managed sibling of cache-aside rather than a separate philosophy.
Write-through, write-back, and write-around
Write-through routes every write through the cache, which synchronously persists to the database before the call returns. A read right after a write always sees current data and no write is ever lost, but you pay two hops on the write path and you populate the cache with entries that might never be read. It fits workloads where read-after-write correctness matters more than write latency.
Write-back, also called write-behind, writes to the cache and returns immediately, then flushes to the database asynchronously, usually in batches. Writes feel instant, and repeated updates to the same key collapse into a single database write, which is exactly what you want for view counts and metrics rollups. The tradeoff is unforgiving: if the cache node dies before it flushes, those buffered writes vanish. Reach for it only where losing a few seconds of recent writes is survivable, and name that risk before the interviewer does.
Write-around sends writes straight to the database and skips the cache, letting the next read populate the entry lazily. That keeps write-once data from evicting hotter keys, but it guarantees a miss the first time anyone reads the value back. Pairing cache-aside reads with write-around writes is an unglamorous, correct choice for read-heavy systems, and saying so signals you’ve thought past the happy path.
| Strategy | Where writes go | Read freshness after a write | Main risk | Good fit |
|---|---|---|---|---|
| Cache-aside (lazy load) | App writes the DB, then invalidates or updates the cache | Depends on invalidation quality | Stale entry if an invalidation is missed | General read-heavy default |
| Read-through | Same as cache-aside; cache loads on a miss | Depends on invalidation quality | Binds you to a cache that supports it | Cleaner read path, managed caches |
| Write-through | Cache, then synchronously to the DB | Always fresh | Slower writes; caches unread data | Read-after-write correctness matters |
| Write-back (write-behind) | Cache now, DB flushed asynchronously | Fresh from cache; DB lags briefly | Data loss if the cache dies before a flush | Write-heavy counters tolerant of loss |
| Write-around | Straight to the DB; cache skipped | Stale until the next read repopulates | Guaranteed miss on first read-back | Write-once, rarely-read data |
What gets evicted when memory fills up
A cache is a bounded store, so at some point a new entry means an old one has to leave. The eviction policy decides which. Least Recently Used drops the entry untouched for the longest, which suits workloads where recent access predicts future access, and it’s the sensible default. Least Frequently Used counts total accesses and protects keys that stay hot over time, which beats LRU when a small set of items is popular for hours but occasionally goes quiet for a minute. FIFO evicts in insertion order and mostly exists as a baseline nobody should pick on purpose. Random eviction sounds naive but performs surprisingly well and costs almost nothing to track, which is why it shows up in real systems.
If your interviewer works in Redis terms, the concrete knob is maxmemory-policy. The allkeys-lru and allkeys-lfu settings consider every key, while the volatile-lru, volatile-lfu, and volatile-ttl variants only evict keys that carry a TTL, leaving unmarked keys pinned in memory. The noeviction setting makes writes fail once memory is full rather than dropping anything, which is the safe choice when the cache also acts as a source of truth for some keys. Knowing these are named settings and not abstractions tends to land well.
Invalidation is the part everyone underestimates
Phil Karlton’s line that “there are only two hard things in Computer Science: cache invalidation and naming things” survives because the first half is true. Getting data into a cache is easy. Deciding when it’s wrong is where systems rot. The blunt tool is a TTL: attach an expiry and accept bounded staleness. Short TTLs keep data fresh and hammer the database with misses; long TTLs cut load and serve stale reads. Picking the number is a product question about how wrong the data is allowed to be, not a technical one.
When staleness isn’t acceptable, you invalidate explicitly: on a write, delete or overwrite the affected cache key. This is precise, and also where subtle bugs breed, because you have to find every key a write touches, including derived and aggregated ones. A durable trick is versioned or immutable keys: bake a version number or content hash into the key so a change writes a brand-new key, and old readers simply stop referencing the old one, which then ages out on its own. That sidesteps the coordination problem at the cost of some wasted memory.
Cache stampede, and how to not melt the database
This is the failure mode interviewers love, because it separates people who’ve been paged from people who’ve only read blog posts. A popular key expires. In the same instant, a thousand in-flight requests all miss, all hit the database, and all try to recompute and repopulate the same value. The database sees a thousandfold spike on one query and can fall over, taking the service with it. This is the thundering herd, or cache stampede.
The cheapest fix is request coalescing, sometimes called singleflight: while a key is being recomputed, the first request does the work and every other request for that key waits and shares the result instead of piling onto the database. Across multiple servers you get the same effect with a distributed lock, commonly a Redis SETNX with a short expiry, so only one process rebuilds the key while the rest briefly wait or serve stale. A complementary defense is TTL jitter: instead of expiring a batch of related keys at the same second, add a random spread so their expirations scatter and never all miss together. The more advanced version is probabilistic early recomputation, where a request approaching the TTL boundary refreshes the key ahead of expiry with a probability that climbs as expiry nears, so the value gets rebuilt before it ever goes cold.
The follow-ups tend to cluster, and having crisp answers ready is most of the battle:
- “Your cache and database disagree. Walk me through how that happened and how you’d bound it.”
- “A single celebrity key gets a million reads a second. What breaks, and what do you do about it?”
- “The cache cluster restarts cold at peak traffic. What happens in the next thirty seconds?”
None of those are about picking write-through over write-back in the abstract. They’re about knowing the failure mode of whatever you picked and having a specific mitigation with a name. Default to cache-aside plus write-around, reach for write-through when a read has to see the last write, keep write-back for lossy counters, and be ready to say “SETNX lock” or “TTL jitter” the second someone mentions a hot key expiring. That’s the gap between a cache that sounds good on a whiteboard and one that stays up.
Keep sharpening your system design:
