Deleting the cache resurrected the old price
A product price changes from 40 to 45 in a hypothetical catalog. The database commits 45; the writer deletes the cache entry. A moment later the cache contains 40 again.
The invalidation happened after commit. The advice was followed. Find the request that crossed the boundary.
Reconstruct the stale refill
| Step | Reader | Writer | Cache |
|---|---|---|---|
| 1 | Misses, reads version 7 at price 40 | empty | |
| 2 | Pauses before filling | Commits version 8 at price 45 | empty |
| 3 | Deletes entry | empty | |
| 4 | Stores version 7 | price 40 |
Would āfill only if the key is absentā fix this trace? Would delaying the deletion by a fixed number of milliseconds prove correctness?
Leave a version behind
An invalidation can advance a per-key minimum acceptable version instead of erasing all knowledge. A fill carries the version it read; one atomic cache operation checks that version against the floor before publishing.
pseudocodeinvalidate(key, version): atomically: floor[key] = max(floor[key], version) discard cached value if its version is below floor[key] fill(key, value, version): atomically: reject if version < floor[key] reject if version < cached_version[key] otherwise install (value, version)
This pseudocode assumes one authoritative version order per key. The floor must survive long enough to reject every stale refill that can still arrive, or the refill path must revalidate another way. Evicting the floor while an old request is in flight reopens the race. A version-specific cache key can simplify immutable object caching, but finding the current version becomes its own freshness problem.
A cache-issued refill lease can also prevent old fills if invalidation revokes outstanding fill tokens. Yet it does not make a lagging database replica fresh. A perfectly valid new fill token can fetch an old value from that replica. Choose the read source, wait for an applied position, or reject versions below the required floor.
Choose what a write promises
Cache-aside makes the database authoritative and fills on demand. Read-through moves the fill into a cache layer. Write-through updates through that layer, but ordering between concurrent writers still matters. Write-behind acknowledges before the backing database is updated; the cache/log pipeline now owns durability and recovery rather than merely accelerating reads. Write-around avoids caching writes until a read needs them.
These are lifecycle choices, not consistency levels. āWrite-throughā alone does not prove ordered updates, and āinvalidateā alone does not prove fresh refills.
Use a reliable change stream or outbox to repair missed application invalidations. Monitor invalidation lag. A TTL can bound how long an installed entry remains under a stated cache model, but a fill from a lagging source may restart that TTL with already-old data. Bound source lag and refill lifetime if promising a total staleness bound.
The author wants to see her own edit
Return the committed version to the editor. On a subsequent read, skip cached versions below that token and read a source that has applied at least that version. This preserves read-your-writes for that session without requiring every public reader to receive the newest value immediately.
A deletion also needs a version. Otherwise delayed updates can resurrect it downstream. Negative caching must be invalidated or superseded on creation; ānot foundā is a cached result with a lifetime, not eternal truth.
Mental model: cache coherence is an ordering protocol between updates and fills. Deleting a value does not delete an in-flight reader's memory.
Transfer: the second region
Region A commits version 9 and immediately tells region B to invalidate. B's database still serves version 8. What must B avoid?
Source trail: 04-distributed-cache-patterns-and-eviction, 11-caching-and-invalidation. See the source map for consolidation and reference-retained material.