2 system design questions lean on this idea. Each walks through the full answer.
Cache invalidation is famous for being hard, and the reason is specific rather than general. It depends entirely on what kind of thing you cached.

A write tells you which row it changed. It does not tell you which cached aggregates, lists, counts or rendered pages were built from that row, and nothing in the write knows.
Every technique here is a different way of solving one problem: given a write, find the derived entries that are now wrong.
The script is at scripts/labs/invalidation/strategies.py. It runs one stream of 120,000 reads and writes through four strategies and counts two things: stale reads served, and delete operations performed.

Deleting the exact key on every write performed 5,779 deletes and served 249.1 stale reads per thousand, against 104.4 for doing no write-side work at all.

This is the result the descriptions cannot give you. Delete on write sounds strictly better than a , costs real work on every write, and on this workload produced more than twice the staleness.

Both strategies were equally blind to derived entries. The one that kept more entries alive served more of them, and every one it served was wrong. A TTL is an invalidation mechanism that works on things you have not thought about.

Read this against the previous chart. Freshness is bought with write-side work, and the price rises with how broadly your derived entries are defined. Versioned keys reached zero staleness with zero deletes, which needs explaining.

Nothing is ever removed, which is why it reaches zero staleness with zero deletes. The orphaned entries remain until the eviction policy reclaims them, so versioning converts an invalidation problem into an eviction problem.

Correctness is not the axis that separates the useful ones. Tag based charges write-side work, versioned keys charge memory, and flushing charges your entire hit rate.


Invalidation works by every writer participating, and the set of writers grows: a migration, an admin tool, a bulk import, another service writing directly. Nothing fails visibly, so the decay is silent.

Invalidation is a property of a whole chain, not of one cache. A design where every layer is invalidated except the last has a worst case equal to the max-age you set on the layer you cannot reach.


An entry defined by a predicate can be invalidated by a row that was never part of it. This is why cache invalidation is called hard: for those entries, the information needed to do the bookkeeping does not exist in the write.

What you name a key decides how you can invalidate it, and it is chosen before any of the difficulty appears. Nearly every impossible invalidation problem was created by a key that could have carried a parent and a version and instead carried a hash.
4 questions - Score 80% to pass
You add per-row delete-on-write invalidation to a cache that previously used only a TTL. Measured, what happened to stale reads?
A cached entry is defined by a filter, such as all orders over £100. Which invalidation strategy can catch a newly inserted matching row?
Should you delete the cache key before or after the database write?
Your invalidation tests all pass. What do they most likely fail to cover?
Write first, then delete. It is one line of ordering and it shrinks the dangerous window from the duration of your write to the duration of one cache call, and the residual case needs the version check the application cache lesson measured.
Over-invalidation is never reported as a bug, because nothing is wrong. It shows up as a hit rate lower than anybody expects and a database busier than it should be, and the cause is a wildcard somebody added to be safe.

Do not skip step one because step five is correct. Tag based invalidation is correct on the day it ships, and the migration script written six months later does not know about it.

The code that invalidates and the code that writes were authored together, so the test passes by construction. The paths that break it are written later, elsewhere, by people who do not know the cache exists.

This would have caught every failure in this lesson. The strategy that served 249 stale reads per thousand passes every test written the usual way, because the usual way only exercises the path that was already correct.

A stale entry produces a hit, no error and no . Add sampled staleness before anything else: it is the only metric that would have caught the 249 stale reads per thousand, and every other number looked healthy while that was happening.

An invalidation driven by the database's own change stream cannot be bypassed by a migration script or another service, because all of them produce the same events. It is also a real system to operate, and a underneath targeted invalidation gets most of the benefit for none of the cost.

The second is the one to keep. Everything else here requires somebody to remember something, and a TTL does not, which is why it belongs underneath every other mechanism and not instead of them.

Both of the famously hard problems in this area are the same decision seen twice. What you name a cache key determines what you are able to invalidate, and that decision is made at the beginning, when none of the difficulty is visible yet.