Say “we’ll put a cache in front of the database” in a system design interview and watch what the interviewer does next. The sharp ones don’t nod along. They ask what happens when the cache is cold on deploy, or when two requests update the same row at once, or when the Redis box reboots at 3 a.m. and every read lands on the database at the same instant. Adding the cache is the easy part. Almost every hard follow-up in the round comes from what that cache does once reality stops cooperating.
A cache earns its place under a narrow set of conditions, and saying them out loud is half the battle. Reads should heavily outnumber writes. A small set of keys should take most of the traffic. And a miss should cost enough that skipping it matters, whether that’s a slow join, a fan-out across services, or a call to something you pay for per request. A product catalog read ten thousand times a minute and edited twice a day fits perfectly. A personalized feed that changes on every tap does not, and putting a cache there is how you serve stale data to the one user certain to notice.
Cache-aside is the default, and it stays quiet about writes
The pattern nearly everyone reaches for is cache-aside, sometimes called lazy loading. The application checks the cache first. On a hit it returns right away. On a miss it reads the database, writes the value into the cache with a TTL, and returns it. People pick it for a solid reason: if the cache dies, the application keeps serving, just slower, because every read falls through to the database. That fallback behavior is why it shows up in nearly every real system.
Writes are where it bites. Cache-aside says nothing about them, so the decision is yours. Update the database and leave the old value sitting in the cache, and readers get stale data until the TTL runs out. Update the database and then overwrite the cache with the new value, and you’ve opened a race: two concurrent writers can interleave their database write and their cache write so the cache ends up pinned to the older value, and it stays wrong until something evicts it. The safer move most teams land on is to write the database, then delete the cache key and let the next reader repopulate it. Delete, don’t update. A missing key just triggers one more database read. A wrong key is a bug that every reader inherits until the TTL bails you out.
Write-through, write-back, and read-through
Once you’ve placed a cache, the interviewer will often ask how writes flow through it, and that’s really a question about which consistency you’re willing to pay for. Four patterns cover almost everything you’ll be asked to compare.
| Caching pattern | What populates the cache | Consistency you get | Write latency | Main failure mode |
|---|---|---|---|---|
| Cache-aside (lazy loading) | Application code, on a read miss | Eventual; stale until the key is deleted or its TTL expires | Unchanged; writes skip the cache | Stale reads after a write, and stampedes when a hot key expires cold |
| Read-through | The cache layer itself, on a read miss | Same as cache-aside for reads | Unchanged | A cache outage blocks reads unless you add an explicit fallback |
| Write-through | The cache, synchronously on every write | Cache and database stay in sync | Higher; every write hits both cache and database | The cache sits on the write path, so a cache write failure can block the write |
| Write-back (write-behind) | The cache, buffering writes and flushing to the database later | Fresh at the cache, database lags behind | Lowest; the write returns once the cache accepts it | Data loss if the cache dies before the buffered writes flush |
Read-through looks like cache-aside but hides the miss handling inside the cache layer, so your application code just asks the cache and never touches the database directly. It reads cleaner, at the cost of a fallback path you no longer control. Write-through keeps the cache and database in lockstep by writing both on every write, which you want for data you can’t afford to read stale, and pay for with slower writes and a cache that now sits on the write path. Write-back buffers writes in the cache and flushes them to the database later, which gives you the fastest writes and the scariest failure mode, since a cache crash before the flush loses data the client already thinks is saved. Most designs run cache-aside for the bulk of reads and reserve write-through for the handful of fields that can’t drift.
The stampede that starts one second after a key expires
Here’s the failure that separates people who’ve run caches in production from people who’ve only read about them. You have one very popular key, say the homepage feed, cached with a 60-second TTL. It expires. In the next few milliseconds, before anyone has repopulated it, a thousand concurrent requests all miss, all decide they need to rebuild the value, and all hit the database at once. One query became a thousand. The connection pool saturates, latency climbs, requests that had nothing to do with that key start timing out, and now you have an outage that looks like a database problem but started in the cache. This is the thundering herd, also called a cache stampede.
The cleanest fix is request coalescing: while a key is being rebuilt, the first request does the work and every other request for that key waits and shares the single result. Go ships this as singleflight, and Facebook’s memcache design solved the same problem with leases, handing exactly one requester a token to recompute a key while the rest briefly wait or serve the last good value. You can pair that with jittered TTLs so popular keys don’t all expire on the same tick, and with background refresh that recomputes a hot key shortly before it expires instead of waiting for a miss. There’s also probabilistic early recomputation, where each read carries a small, rising chance of refreshing the key as it nears expiry, which spreads the rebuild across time and across requesters. Naming even one of these signals you’ve felt this pain.
Invalidation is where the real bugs live
There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton
The joke lands because invalidation has no clean answer, only trade-offs you pick on purpose. A TTL is the safety net you should never skip, even with delete-on-write, because it bounds how wrong any single key can get. If you delete on every write and one delete gets dropped, a 5-minute TTL means the damage clears in 5 minutes instead of never. Choosing that number is the interesting part. Too long and users see stale data; too short and your hit ratio collapses and the cache stops paying for itself. Set it against how much staleness the data can tolerate. A product description can be minutes stale without anyone caring. A displayed account balance cannot, and a permissions or feature-flag check must not, because a stale “allow” is a security bug, not a display glitch.
This is also where interviewers probe whether you understand what you’re actually caching. Caching a user’s list of permissions to save a database call feels harmless until you revoke access and the old answer lingers for the length of the TTL. The usual answers are to keep authorization decisions out of the cache, to invalidate them explicitly on every change rather than trusting a timer, or to hold the TTL short enough that the exposure window is something you can defend to a security reviewer.
The follow-ups tend to arrive in a predictable shape, so it helps to have felt them before:
- “Your cache hit rate is 95% and the database still falls over at peak. What’s going on?”
- “A user updates their profile and still sees the old version on refresh. Walk me through where the bug is.”
- “This key gets a million reads a second. What happens the moment it expires, and how do you keep the database standing?”
What a strong answer actually sounds like
A good answer never opens with “add Redis.” It opens with the access pattern: how skewed the reads are, how much staleness the data tolerates, what a miss actually costs. Only then does the cache take a shape, with a write policy, a TTL set against the tolerance you just named, and a plan for the second a hot key expires under load. Candidates who struggle treat the cache as free speed you sprinkle on at the end. The ones who do well treat it as a second copy of the truth that can drift from the source, and they can tell you exactly when it will drift and what they’ve done about it. That second copy is the whole job. Everything the interviewer asks after “add a cache” is really asking whether you remember it’s there.
Keep sharpening your system design:
