Cache invalidation and update strategies
Cache-Aside, Write-Through, Write-Behind (Write-Back), and Refresh-Ahead patterns.
"There are only two hard things in Computer Science: cache invalidation and naming things." — a saying usually attributed to Phil Karlton
When data changes in the database, cached copies become stale. The cache update strategy decides when the cache learns about the change, and so what you trade between write speed, read speed and staleness.
What you will learn
- Compare cache-aside, write-through, write-behind and refresh-ahead.
- Explain why a write should delete the cache entry rather than set it.
- Close the stale-set gap with leases.
The four canonical cache update strategies
1. Cache-aside (lazy loading)
The application talks to both the cache and the database; the cache never talks to the database:
- Read path: Look in the cache; on a miss, load from the database, add the entry to the cache (usually with a TTL), and return it.
- Write path: Write to the database, then delete the cache entry so the next read reloads it. Facebook's memcache works this way.
- Pros: Only requested data is cached; later reads of it are fast. If the cache fails, the application still works, just more slowly.
- Cons: Each miss costs three trips (cache, database, cache). Data can be stale until its TTL expires. A replacement cache node starts empty, so latency rises until it warms up.
2. Write-through
The application uses the cache as its main data store; the cache reads from and writes to the database:
- Write path: Write the entry to the cache, which writes it synchronously to the database before returning.
- Pros: Data in the cache is not stale, and reads of just-written data are fast.
- Cons: Every write is slow (users usually tolerate slow updates better than slow reads). A new cache node holds nothing until entries are written again (combining with cache-aside helps), and much of what is written may never be read (a TTL helps).
3. Write-behind (write-back)
Write the entry to the cache, which writes it to the database asynchronously:
- Pros: Faster writes, and the cache can combine several updates before writing.
- Cons: Data is lost if the cache goes down before its contents reach the database, and it is more complex to build than cache-aside or write-through.
4. Refresh-ahead
The cache automatically refreshes recently accessed entries shortly before they expire:
- Pros: Lower latency than read-through, if the cache accurately predicts which items will be needed.
- Cons: Poor predictions can make performance worse than not refreshing at all.
Invalidation vs. update: why delete, not set, on write
A common anti-pattern is attempting to update the cache on write:
// ANTI-PATTERN (Vulnerable to Race Conditions):
await db.updateUser(user);
await cache.set(user.id, user); // WRONG!
If Thread A and Thread B update the same record concurrently:
- Thread A writes to Database.
- Thread B writes to Database.
- Thread B updates Cache to .
- Thread A (delayed by network jitter) updates Cache to .
- Result: Database has , but Cache has . The cache is permanently corrupted!
Better pattern: delete (invalidate) the cache key after the database write. Deletes are idempotent, so repeating or reordering them is harmless:
await db.updateUser(user);
await cache.del(user.id); // the next read reloads the current value from the database
Deleting does not close every gap. A reader can miss, read from the database, stall, and then put back into the cache after a writer has stored and deleted the key: a stale set. Facebook's memcache fixes this with leases: on a miss the cache hands the reader a token, a delete invalidates outstanding tokens, and a set with an invalidated token is rejected. The same leases also limit thundering herds (next lesson).
Cache-aside, write-through and write-behind
Orchestrates cache lookups and DB transactions.
Zero I/O. Database CPU completely offloaded by cache hit.
1. App checks Cache → 2a. Hit: returns data; 2b. Miss: App reads DB → App populates Cache.
1. App updates DB → 2. App invalidates (deletes) cache key. (Never update cache directly on write).
Hit: 1.2ms | Miss: 28ms
Eventual (Brief window during concurrent writes; mitigated by TTL)
Key takeaways
- Cache-aside caches only what is read and survives a cache failure, but each miss costs three trips and entries can be stale until their TTL.
- Write-through keeps the cache fresh at the cost of slow writes; write-behind makes writes fast but can lose data; refresh-ahead helps only if it predicts well.
- Setting the cache on write lets two concurrent writers leave the old value cached; deleting is idempotent and safe to repeat or reorder.
- A slow reader can still put an old value back after a delete; leases, as in Facebook's memcache, reject that stale set.
Sources
- System Design Primer: Cache-aside, write-through, write-behind, refresh-ahead
- Nishtala et al., "Scaling Memcache at Facebook", NSDI 2013 (look-aside, delete on write, leases)
- Martin Fowler, "TwoHardThings"
Adapted from The System Design Primer by Donne Martin, licensed under CC BY 4.0. Changes were made.