If you remember five things
The whole answer in one glance. Everything below defends these five calls.
- Every cached entry is a bet. You are betting the source has not changed since you stored it. Invalidation is how you settle the bet: on a timer, on an event, or lazily.
- Delete, do not update. Deleting the key on write defers correctness to read time, where it is easy. Updating couples cache correctness to write ordering, where two concurrent writes race and the stale one wins.
- Order matters: DB first, then delete. Delete before the write and a concurrent read can repopulate stale data ahead of you. Delete after, often twice with a short delay, to close the race.
- Always keep a TTL backstop. Even on write-invalidated keys. A dropped invalidation stays wrong forever without one. The TTL bounds worst-case staleness instead of leaving it unbounded.
- Invalidation causes stampedes. The expiry event itself hammers your database. Single-flight plus jittered TTLs is the fix, and volunteering it before you are asked is the staff-level signal.
1The core idea#
A cache holds a copy of data that lives authoritatively somewhere else: a database, another service, a slow computation. Invalidation is the set of rules that decide when a cached copy is no longer trustworthy.
The mental model I use: every cached entry is a bet.
- You are betting the underlying data has not changed since you stored it.
- Invalidation is how you settle the bet.
- You can settle it on a timer (TTL), on an event (something wrote to the database), or lazily (check on read).
- Each choice trades freshness against cost and complexity.
Why the old joke actually lands.
- The line is that there are only two hard things in computer science: cache invalidation and naming things.
- Reads are cheap and forgiving.
- Writes are where everything goes wrong, because a write means some cached copy, maybe on a different machine, maybe in a user's browser, is now stale and you may not even know it exists.
Two concepts interviewers conflate and good candidates separate:
- Staleness. The cache returns old data. Sometimes fine (a product view count), sometimes a lawsuit (a bank balance).
- Invalidation lag. The time between the write landing in the database and every cache copy reflecting it. You almost never get this to zero; the honest goal is bounding it.
Size the lag directly, because these three numbers pick your strategy:
- Same-datacenter Redis delete: under 5 ms.
- Cross-region event over Kafka: roughly 200 ms to 2 seconds.
- CDN purge: 10 to 60 seconds.
Your mental model (the cache) and reality (the database) can and will diverge. You do not get to pretend otherwise. The whole skill is bounding the divergence: name the staleness window, name the race, name the backstop. "It just works" loses the room.
2How it works#
There are three families of invalidation, and most real systems blend them.
TTL (time to live). Stamp each entry with an expiry.
- After, say, 60 seconds, the next read misses and reloads from the source.
- Dead simple: no coordination, no write-path involvement.
- The cost is a built-in staleness window equal to the TTL.
- A 5 minute TTL tells users "you might see data up to 5 minutes old," full stop.
Write-driven invalidation. When the app writes to the database, it also tells the cache. Two flavors:
- Write-through / write-update: on write, update the cache with the new value too.
- Write-invalidate: on write, just delete the cache key. The next read repopulates it.
I almost always delete instead of update. Here is the race that forces the call:
- Updating on write sounds efficient, but two concurrent writes can interleave so the cache keeps the older value.
- Writer A sets price to 100, writer B sets price to 120.
- A's cache write lands at Redis after B's because of a GC pause or a slow network hop.
- Now the database says 120, the cache says 100, and nothing self-corrects until the entry is touched again.
- Delete sidesteps all of it: the repopulating read always pulls current database state. Less clever, far fewer 2am pages.
Event-driven invalidation. The database or a change-data-capture stream publishes change events, and a consumer invalidates affected keys.
- Decouples the write path from cache knowledge.
- Scales to many caches and services.
- It is how you keep caches across regions roughly in sync.
Here is the standard cache-aside read-and-write flow I expect a candidate to be able to draw:
And the shape of the system around it:
Reveal the answer
3A worked example: the product price page#
Interviews reward numbers over adjectives, so make it concrete. The setup:
- A catalog service: 10 million products.
- 50,000 read requests per second on product detail pages.
- A modest 50 writes per second (price edits, stock updates).
- Postgres comfortably serves maybe 8,000 point reads per second before latency climbs, so you put Redis in front.
With a 300 second TTL and no write hook.
- At a 98% hit rate, the database sees 50,000 x 0.02 = 1,000 reads per second, well inside budget.
- But a price edit can take up to 300 seconds to show.
- For a storefront that runs flash sales, 5 minutes of wrong prices is a support nightmare and possibly a legal one.
Add write-invalidate (delete on write) plus a 300 second TTL backstop.
- A price edit deletes the key, so the next read repopulates from Postgres with the correct price within a few milliseconds.
- Freshness drops from 300 seconds to sub-second for edited products.
- The TTL still catches the case where an invalidation is dropped.
- Extra write cost: 50 deletes per second, trivial.
The failure mode this introduces.
- Your single hottest product, the homepage banner, gets 5,000 requests per second alone.
- Its key expires at second 300.
- In that one millisecond, all 5,000 in-flight requests miss and hit Postgres at once.
- Postgres, sized for 1,000 reads per second, sees a 5,000 read-per-second spike on a single query; latency climbs to seconds and timeouts cascade upstream.
- That is a stampede, and it was caused directly by an invalidation event.
- The fix is single-flight: one request rebuilds the key while the other 4,999 wait on the result or serve the just-expired value (full fix in section 6).
Every cache decision moves the pain somewhere. It does not remove it. Naming where you moved it is the whole game.
4Trade-offs#
This is the heart of the interview. Every strategy is a position on three axes: freshness, write cost, and operational complexity.
| Strategy | Freshness | Write cost | Complexity | Good for |
|---|---|---|---|---|
| TTL only | Weak, bounded by TTL | Zero | Lowest | Tolerant reads, high volume, no write hook |
| Write-invalidate (delete) | Strong-ish, has races | One extra op per write | Low | App owns both read and write paths |
| Write-through (update) | Strong-ish | One extra op, race-prone | Medium | Read-heavy, value cheap to compute |
| Event / CDC driven | Eventually consistent, bounded | Decoupled from write | High | Many caches, multi-region, microservices |
| Versioned keys | Strong, no stale reads | Bump a version | Medium | Grouped invalidation, bursty updates |
A few judgment calls I expect to hear defended:
TTL is underrated.
- Juniors treat it as the lazy option; it is often the right answer precisely because it needs no write path and degrades gracefully.
- If your service tolerates 30 seconds of staleness, a 30 second TTL removes an entire class of invalidation bugs.
- I have replaced fragile event pipelines with a 10 second TTL and slept better.
Delete beats update.
- The interview framing: updating the cache couples correctness to write ordering.
- Deleting defers correctness to read time, where it is easy to get right.
Versioned keys for grouped invalidation.
- Say you cache 10,000 entries that all depend on one tenant's config.
- Instead of tracking and deleting 10,000 keys, embed a version in the key:
tenant:acme:v7:product:42. - Bump to v8 and every old key is instantly orphaned and falls out via eviction.
- One write invalidates a whole family.
- Cost: wasted memory until eviction. At 10,000 keys of roughly 2 KB each, that is about 20 MB sitting dead until the LRU reclaims it, cheap next to tracking 10,000 individual deletes.
Negative caching.
- Do you cache "this does not exist"?
- Skip it and a flood of requests for a missing key hammers your database (cache penetration).
- Cache it and you now must invalidate the negative entry the moment the thing gets created.
- Both choices have teeth.
Write-invalidate (delete)
- Freshness
- Sub-second after a write, modulo the delete race.
- Correctness model
- Deferred to the repopulating read; always pulls current DB state.
- Write cost
- One extra op (a delete) per write.
- Breaks when
- Writes come from systems the service never sees; then you need CDC.
Write-through (update)
- Freshness
- Strong on paper.
- Correctness model
- Coupled to write ordering; two concurrent writes can leave the stale value in cache.
- Write cost
- One extra op, plus a race to reason about.
- Breaks when
- Concurrent writers interleave and the older cache write lands last.
5Where it is used#
Cache invalidation is not one feature. It shows up at every tier.
CDNs.
- A CDN caches static assets and sometimes API responses at the edge, across hundreds of locations.
- Invalidation here is a purge: you call the CDN API to evict a path.
- Purges are relatively slow and sometimes rate-limited.
- So the industry leans on cache busting instead: change the URL. Ship
app.9f3a21.jsinstead ofapp.js, and the old URL simply stops being requested. - That is versioned keys applied to the web.
Database query caches and ORMs.
- Many ORMs cache query results and invalidate dependent query keys on a row update.
- Getting the dependency tracking right is genuinely hard, which is why a lot of teams disable it and use explicit caching.
Application caches (Redis, Memcached).
- The classic cache-aside pattern: read through the cache, invalidate on write.
- This is probably what your interviewer means by default.
HTTP caching.
Cache-Control,ETag, andLast-Modifiedheaders are an invalidation protocol.- An
ETaglets a client ask "has this changed since hash abc123?" and the server replies 304 Not Modified if not, revalidating without resending the body. - This is invalidation pushed to the client, and it is everywhere.
CPU and config.
- The same ideas apply outside web systems: processor caches, in-memory config that reloads on a signal, materialized views that refresh on a schedule.
How I pick, quickly:
- Control the write path and need freshness: write-invalidate.
- Many independent readers, bounded delay acceptable: TTL or CDC events.
- At the edge: versioned URLs, every time.
6Common pitfalls and gotchas#
The thundering herd (cache stampede).
- A popular key expires. In the same millisecond, 5,000 requests miss and all 5,000 hit the database to recompute the same value.
- Your database, sized for cache-hit traffic, falls over.
- This is the single most common production failure caused by invalidation, and interviewers love it.
- Fixes, used together: a lock or lease so only one request recomputes while others wait, request coalescing (single-flight), serving slightly stale data while one worker refreshes in the background, and jittering TTLs so keys do not all expire at the same instant.
A concrete single-flight sketch in Go-flavored pseudocode:
val, err := cache.Get(key)
if hit {
return val
}
// only one goroutine per key actually runs the loader
val, err = group.Do(key, func() (any, error) {
row := db.Query(key)
cache.Set(key, row, ttlWithJitter(300))
return row, nil
})
return val
- That
group.Docollapses 5,000 concurrent misses into a single database query. - The other 4,999 block on the same in-flight call and share its result.
- Jittering the TTL (say 300 seconds plus or minus 30) stops a batch of keys loaded together from all expiring on the same tick.
Stale forever.
- You invalidate on write, but one write path forgot to call the invalidator.
- Now a key is permanently wrong until something evicts it.
- This is why I keep a TTL even on write-invalidated keys: worst-case staleness is bounded by the TTL instead of unbounded.
The double-write race.
- Delete-before-write plus a concurrent read can resurrect old data (covered in section 2).
- Delete-after-write plus a short delayed second delete, or versioned keys, or just a modest TTL, all help.
Invalidating the wrong granularity.
- Caching a whole page but needing to invalidate one widget.
- Or caching per-user when the data is shared.
- Match cache key granularity to how the data actually changes.
Multi-region skew.
- You invalidate in region A, but the event to region B is delayed or lost.
- Users in B see old data for minutes.
- Any multi-region design needs an explicit answer here: usually a durable event stream and a per-region TTL backstop.
Negative cache poisoning.
- You cached "user not found," then the user signs up, and your negative entry blocks them from logging in for the TTL duration.
- Invalidate negative entries on the create path, or keep their TTL very short.
Treating the cache as self-correcting. "It just syncs" or "Redis handles that" tells me you have never been paged for a stale key. Name who deletes the key, when, and what happens when that delete is dropped. If you cannot, I doubt the rest of your design.
7How it shows up in interviews#
Interviewers rarely ask "explain cache invalidation" directly. They slip it in.
Interviewer: You added Redis in front of Postgres, nice. Now the user edits their profile. Walk me through what happens.
That is the trap, and how you answer tells me everything.
Weak answer:
Candidate: The cache updates automatically, so the user sees the new profile right away.
- No. Nothing is automatic.
- This answer never names who deletes the key, never mentions write ordering, never considers a dropped invalidation or a hot-key spike.
- If you cannot name who deletes the key and when, you have not run a cache, and I now doubt everything else you told me about the design.
Strong answer:
Candidate: On profile edit, I write to Postgres first, then delete the cache key, in that order, so a concurrent read cannot repopulate stale data ahead of my write. I delete rather than update to avoid a write-ordering race between two concurrent edits. I keep a 60 second TTL as a backstop so a missed invalidation self-heals within a minute instead of staying wrong forever. And if this profile is a celebrity account getting thousands of reads per second, I wrap the reload in single-flight so one expiry does not stampede Postgres.
- That hits freshness, race safety, failure recovery, and load, with a number attached, in four sentences.
- The difference is not vocabulary. The strong answer assumes things go wrong and bounds the damage.
I am not grading whether you know the words TTL and single-flight. I am grading whether you reach for the backstop and the stampede fix before I ask. Volunteer them. It is the clearest signal that you have actually been paged for this and are not reciting a blog post.
"What if the invalidation message is lost?"
"A key just expired and traffic spiked. What happens?"
"How stale can this data be?"
"Multi-region. How do the caches stay in sync?"
"Why delete the key instead of updating it with the new value?"
What you can now explain in the room
- Why every cached entry is a bet, and the three ways to settle it: timer, event, or lazy read.
- Why delete beats update on write, and the concurrent-write race that forces the choice.
- Why you write the DB first then delete the cache, and why delayed double delete exists.
- How invalidation itself causes a stampede, and how single-flight plus jittered TTLs bound it.
- Why you keep a TTL backstop even on write-invalidated keys.
- How invalidation differs across CDNs, ORMs, Redis, and HTTP, and why the edge uses versioned URLs.
Next, try the System Design Question walkthrough on designing a URL shortener's read path, where cache stampedes and TTL choices decide whether the service survives a viral link.
Get a prep plan built around your Cache Invalidation interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.