Invalidation: TTL, versions, and the delete that arrives late
The hard part of caching is not storing the answer. It is knowing when the answer changed.
What you will be able to do
- Pick an invalidation approach for a given freshness requirement
- Explain the double-delete and version-key techniques
- Describe how event-driven invalidation fails and how to make it reliable
- Define a measurable staleness bound
Four mechanisms
| Mechanism | Freshness | Weakness |
|---|---|---|
| TTL only | Bounded by TTL | Worst case is a full TTL of stale data |
| Delete on write | Near-instant | Races with concurrent reads; keys can be re-cached stale |
| Version in the key | Atomic | Old versions linger until they expire; more keys |
| Change event | Seconds, if reliable | Dropped or reordered messages leave silent staleness |
In practice you combine them: a short TTL as a backstop, event-driven deletion for promptness, and versioned keys where correctness matters most. The TTL is the insurance policy — it guarantees that every mistake eventually heals.
The version-key trick
Instead of storing product:42, store product:42:v7, where 7 comes from the row's version column or a monotonic update counter. A writer bumps the version in the database; readers derive the key from the version they were given. Old entries become unreachable immediately and expire on their own. There is no delete to race, because the key name itself changed.
read(id): v = version_store.get(id) # e.g. Redis or the row itself
hit = cache.get(f"product:{id}:v{v}")
if hit: return hit
row = db.read(id)
cache.set(f"product:{id}:v{row.version}", row, ttl=600)
return row
write(id): row = db.update(...) # version increments inside the transaction
version_store.set(id, row.version)Event-driven invalidation, done honestly
Publish a change event when a row changes, and have every cache consumer delete the relevant keys. The failure mode is that the event is lost, delayed, or applied twice.
Bounded staleness is a product conversation
Write the requirement as a number: profile edits visible within 5 seconds, inventory counts within 30 seconds, recommendation rails within 15 minutes. Then choose a mechanism that provably meets it. Without a number, every discussion becomes a contest of intuition, and someone eventually ships a cache with a one-hour TTL in a flow that needed one second.
Worth remembering
- TTL is the only invalidation that cannot be missed; everything else can drop a message.
- Versioned keys make invalidation atomic: change the version and old entries become unreachable.
- Invalidation events must be delivered at-least-once and be idempotent, or the cache drifts.
- State the maximum staleness in seconds. If nobody can name it, the requirement is missing.