System Design Bits

Support the project

System Design Bits is free and always will be. If it helped, a coffee helps back.

Buy Me a Coffee QR codeBuy me a coffee

Concept 6 · Scaling and Trade-offs·14 min read

Caching Strategies & Cache Invalidation

A second copy of the truth, and every way of keeping it honest.

Big Idea

A cache only helps if it’s usually right and cheap to check, and every caching strategy is really a different answer to one question: when the underlying data changes, how does the cache find out, and what does a reader see in the meantime.

Why This Exists

Why The Hardest Part Is Not The Caching

▸

Databases are slow relative to memory, and the same data (a user's profile, a product page, a popular post) often gets read far more often than it changes, recomputing or refetching it from scratch on every single read wastes capacity that a cache could save entirely.

▸

The moment you add a cache, you've created a second copy of the truth, and now you have to decide what happens when the original changes: does the cached copy update immediately, eventually, or does it just quietly go stale and serve wrong data until something notices.

▸

Phil Karlton's famous line, 'there are only two hard things in computer science: cache invalidation and naming things,' is not a joke about caching being finicky, it's a real acknowledgment that keeping a cache correct under concurrent reads and writes is a genuinely hard distributed systems problem, not an afterthought.

Think Of It Like

A Cheat Sheet Of Your Bank Balance

A cache is like keeping a printed cheat sheet of your bank balance instead of calling the bank every time you want to check it. It’s fast, you glance at the paper instead of waiting on hold. But the moment money moves in or out of the actual account, that paper is wrong until you update it.

Do you update the paper the instant a transaction happens (write-through), let it happen in the background a moment later (write-back), or just throw the paper away after an hour and force a fresh call next time you need it (TTL)? Each choice trades a bit of speed for a bit of risk of looking at stale numbers.

The Concept

The Pieces That Make Up A Caching Decision

1
Cache-aside (lazy loading)

The most common pattern: the application checks the cache first, on a miss it reads from the database, then writes the result into the cache for next time. Reads are fast after the first miss, but the cache and database can drift out of sync if the underlying data changes without going through this same path.

2
Write-through

Every write goes to the cache and the database together, synchronously, before the write is considered done. This keeps the cache always consistent with the database, at the cost of every write now paying the latency of two systems instead of one.

3
Write-back (write-behind)

A write goes to the cache immediately and is considered done, then gets flushed to the database asynchronously later. This makes writes fast, but risks data loss if the cache fails before the flush happens, and needs careful design to avoid it.

4
TTL (time to live)

The simplest invalidation strategy: every cached entry expires automatically after a set duration, forcing a fresh read from the source of truth. It doesn't require any explicit invalidation logic, but it means the cache can serve stale data for up to the full TTL window, and picking that window is itself a trade-off between staleness and load on the database.

5
Explicit invalidation

The application actively deletes or updates a cache entry the moment the underlying data changes, e.g. a user edits their profile, and that write path also deletes the cached profile so the next read is forced to refetch. This gives tighter consistency than TTL alone but requires the application to correctly find and invalidate every cache entry that could be affected by a given write, which gets hard fast as relationships between cached data grow. The usual answer is to track, alongside the cache, which cached entries depend on which underlying thing, so changing that thing can invalidate all of them in one step instead of hoping every write path remembered.

6
Cache stampede (thundering herd)

Happens when a popular cached entry expires and a burst of concurrent requests all miss the cache at the same instant, all hammering the database simultaneously trying to refill it. Mitigations include locking so only one request refills while others wait, or staggering TTLs so not everything expires at the same second.

7
Eviction policy

Decides what gets removed when the cache is full, not just when data goes stale. LRU (least recently used) is the most common default, evicting whatever hasn't been accessed in the longest time, on the assumption that recently used data is likely to be used again soon. When that assumption fails, counting how often an entry is used, with older activity fading over time, keeps genuinely popular data resident while letting a one-off scan pass through without flushing everything.

8
Cache placement

A cache can live in the browser (HTTP caching headers), at the CDN edge, in front of an application server (like Redis or Memcached), or even inside a single process's memory. Each layer trades how close it is against how much it can hold and how many different clients can share it.

9
Choosing what to cache

Matters as much as how: caching data that's read far more often than it's written, and that can tolerate at least some staleness, gives the best return. Caching data that changes on nearly every read, or where staleness is unacceptable (like real-time account balances), often isn't worth the complexity a cache adds.

10
Cache penetration

Requests that ask for something which never existed, like a probe for user ID -1 or a stream of invented identifiers. Every one misses the cache, reaches the database, finds nothing, and writes nothing back, so the next identical request repeats the whole trip. Unlike a stampede this can be driven deliberately, and the fix is counter-intuitive: cache the "not found" answer too, with a short expiry, so the second request stops at the cache.

11
Avalanche vs breakdown

Two shapes of the same problem. Breakdown is one popular key expiring and sending a focused burst at the database. Avalanche is thousands of unrelated keys, all written at the same moment by a restart or a nightly job, expiring within the same second and shifting their combined traffic at once. Locking fixes the first, because there is one key to queue behind. Only spreading expiry times apart fixes the second.

12
Protecting what you cannot lose

When one cache holds both disposable data and data that must survive, like sign-in sessions alongside cached query results, a blanket eviction policy will happily throw the sessions away to make room. The usual answer is to let eviction consider only entries that carry an expiry, and give an expiry only to the disposable ones, so anything without a deadline is never a candidate for removal.

Key Terms

The Vocabulary Of A Second Copy

Cache-aside

A pattern where the application checks the cache first, falls back to the database on a miss, and populates the cache afterward, the most common general-purpose caching pattern.

TTL (time to live)

A duration after which a cached entry automatically expires and must be refetched from the source of truth, the simplest and most common invalidation mechanism.

Cache stampede

A surge of simultaneous requests all missing the cache at once (often right after a popular entry expires) and overwhelming the database trying to refill it.

Eviction policy

The rule that decides which entries get removed when a cache reaches capacity, most commonly LRU (least recently used).

Write-through cache

A caching strategy where writes update the cache and the underlying database synchronously, keeping them always consistent at the cost of write latency.

Stale data

Cached data that no longer matches the current state of the source of truth, the central risk every invalidation strategy is trying to manage.

Cache penetration

Repeated requests for data that does not exist anywhere, which miss the cache every time and reach the database every time, because there is no result to store.

Cache avalanche

Many cached entries expiring at nearly the same moment, usually because they were all written together, sending their combined read traffic at the database at once.

Worked Example

Write-Through vs Write-Back

Write-through
●

Write goes to cache and database together, synchronously

●

Cache and database always agree

●

Every write pays the cost of two systems

VS
Write-back
●

Write goes to cache immediately, database updated later

●

Writes are fast

●

Risk of data loss if the cache fails before flushing

The Shape Of It

Cache-Aside On A Read

1
Check cache

is the data there?

2
Cache miss

not found or expired

3
Read from database

the source of truth

4
Write result into cache

for next time

5
Return to caller

future reads hit the cache

Step 4 is the one that bites. Nothing here tells the cache when the database changes through some other path, so a write that skips this flow leaves the entry stale until its TTL runs out or something explicitly deletes it.

Failure Mode

When The Cache Stops Absorbing Load

Stampede: a popular key expires

Unprotected refill

Cache stampede without a refill lockRequest 1Request 2..NCacheDatabaseread (miss, expired)read (miss too)N identical queries, all at onceN identical results

Locked refill

Cache stampede prevented by a refill lockRequest 1Request 2..NCacheDatabaseread (miss, expired)acquire refill lockread (miss too)wait, refill in progresssingle query to refillfresh valuewrite value, release lockserve fresh cached value
The database load difference is the whole point: N queries versus one, at the exact moment a popular key expires. A lock works here because there is a single key for everyone to queue behind.

Penetration: a key that never existed

A stampede needs a real entry to expire. Penetration needs nothing to exist at all. Requests arrive for an identifier that was never in the database, perhaps a probe for user ID -1 or a stream of invented IDs. Each one misses the cache, reaches the database, finds nothing, and writes nothing back, so the cache never learns and the next identical request makes the same trip.

That is the part worth sitting with: a cache only shields the database from questions it has already answered, and “nothing found” never gets recorded as an answer. The fix is to record it anyway, storing the empty result with a short expiry so the second request stops at the cache. Short, because the record might genuinely appear later.

Unlike a stampede, which is an unlucky coincidence of timing, penetration can be driven deliberately. Anyone who can send requests can pick identifiers that will never hit, which makes it a denial-of-service technique as much as a performance bug.

Seen In The Wild

Where The Read-To-Write Ratio Pays

▪

Reddit and Twitter/X use Memcached and Redis extensively to cache hot content (popular posts, feeds) so millions of reads don't hit the database directly for the same rapidly re-requested data.

▪

CDNs like Cloudflare and Akamai apply TTL-based caching to static assets at the edge, letting a browser or edge node serve a cached copy for minutes to days without contacting the origin server at all.

▪

Facebook's TAO caching layer sits in front of its social graph database specifically to absorb the enormous read-to-write ratio of social data, most content is read far more often than it's ever edited.

▪

Amazon's product pages use aggressive caching with careful invalidation on price and inventory changes, since showing a stale price is a real business problem, illustrating how the choice of TTL versus explicit invalidation depends on how costly staleness actually is.

Key Points

What To Carry Forward

✓

Every caching strategy answers the same question differently: how does the cache learn the source of truth changed, and what does a reader see before it does.

✓

Cache-aside is the default pattern: check cache, miss, read database, populate cache.

✓

Write-through keeps cache and database always in sync at the cost of write latency; write-back is fast but risks losing data before it's flushed.

✓

TTL is the simplest invalidation approach but trades correctness for simplicity, entries can be stale for up to the full TTL window.

✓

Cache stampede is a real failure mode when a hot entry expires and many requests hit the database at once, mitigated with locking or staggered expiration.

✓

Cache what's read far more than it's written and can tolerate some staleness, that's where caching pays off the most.

Common Mistakes

Where This Usually Goes Wrong

✕

Adding a cache without a real invalidation plan, treating 'it'll expire eventually' as sufficient for data where staleness actually matters.

✕

Using a TTL so long that users see meaningfully outdated data, or so short that the cache barely reduces database load at all, without deliberately choosing the trade-off.

✕

Not accounting for cache stampede, letting a single hot key's expiration cause a burst of simultaneous database load right when the system can least afford it.

✕

Caching data that changes on nearly every read, adding cache complexity and a consistency risk for a hit rate that's barely better than not caching at all.

✕

Forgetting that a cache is a second copy of the truth, and every code path that writes the underlying data needs to also handle the cache, not just the 'main' write path.

✕

Never caching the answer 'this does not exist', so a stream of requests for invented identifiers passes straight through the cache to the database every single time.

“I keep a mental cache of which humans give treats on command. Never invalidated it once. That’s why I still try the trick on the mail carrier.”

Try It Yourself

Two Ways To Keep It Fresh

Pick a piece of data in an app you use that changes rarely but is read constantly, like a user’s display name or a product’s description. Sketch out, in a sentence each, how you’d cache it with a TTL versus with explicit invalidation on write, and name one scenario where each approach would show a user outdated information.

Quiz Review

Check your understanding

Question 1 of 6

Why is cache invalidation considered a genuinely hard problem instead of just an implementation detail?

  • ✓Because it requires knowing, at the moment underlying data changes, every cached representation of that data that now needs to be updated or removed, across potentially many cache keys, layers, and services, while concurrent reads and writes keep happening. Getting this wrong doesn't crash the system, it silently serves incorrect data, which is often worse than an outage because nobody notices immediately.