AnyLearn
All lessons
Computer Scienceintermediate

Caching Strategies

The named caching patterns (cache-aside, read-through, write-through, write-behind), when each makes sense, and the failure modes that bite even experienced teams (thundering herd, stale invalidation, the second-hardest problem).

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 8

What a cache buys you

A cache is a fast store sitting in front of a slow one. The trade you're making: memory for time, accepting that the cached copy can lag the source.

What you should expect:

  • 10–1000x lower latency on hits.
  • 10–100x lower cost per read at scale (a cache hit doesn't touch your DB).
  • A new failure mode: stale data. Cached copies can disagree with the source of truth.

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton. Naming is jokingly the harder one; invalidation is the one that actually keeps people awake.

Full lesson text

All 8 steps on one page, for reading, reference, and search.

Show

1. What a cache buys you

A cache is a fast store sitting in front of a slow one. The trade you're making: memory for time, accepting that the cached copy can lag the source.

What you should expect:

  • 10–1000x lower latency on hits.
  • 10–100x lower cost per read at scale (a cache hit doesn't touch your DB).
  • A new failure mode: stale data. Cached copies can disagree with the source of truth.

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton. Naming is jokingly the harder one; invalidation is the one that actually keeps people awake.

2. Cache-aside (the default)

The most common pattern. The application code talks to both the cache and the database. The cache itself knows nothing about the DB.

def get_user(user_id):
    user = cache.get(f"user:{user_id}")
    if user is not None:
        return user
    user = db.fetch("SELECT * FROM users WHERE id = %s", user_id)
    cache.set(f"user:{user_id}", user, ttl=300)
    return user

Pros: simple, cache can hold arbitrary keys, DB stays the source of truth. Cons: every miss is a DB hit; writes have to invalidate the cache manually (cache.delete after db.update), and forgetting that delete is a common bug.

3. Read-through and write-through

Read-through: the application talks only to the cache. The cache talks to the DB on a miss. Same caller code as cache-aside, but the responsibility moves to a cache layer that knows your data model.

Write-through: writes go to the cache first, which synchronously writes to the DB. The cache is never out of sync.

App.write(key, value)
  └─> Cache.put(key, value)        # 1. update cache
       └─> DB.write(key, value)     # 2. then DB, atomically

Pros: no stale data, application code is simpler. Cons: every write pays cache + DB latency. Both stores must be available, or writes fail.

In practice: read-through is what managed caches (DAX for DynamoDB, some Redis layers) automate. Write-through is rare in raw form; dual-writes with a write-aside is more common — and famously hard to get right.

4. Write-behind: speed at a price

Write-behind (write-back) lets writes hit the cache and return immediately. The cache batches and asynchronously writes to the DB later.

Used when:

  • Write throughput exceeds DB write capacity.
  • A small acknowledged-but-not-yet-durable window is acceptable.
  • You can afford an out-of-process buffer (Kafka, a queue) to make the deferred write reliable.

The risk: data loss on cache crash. The user thinks the write succeeded; the cache hadn't flushed it; node dies. Disabled by default for anything money-shaped. Use it for telemetry, hot counters, like buttons — places where dropping 0.01% of writes is tolerable for 100x write throughput.

5. Cache-aside vs write-through vs write-behind

Where the write hop sits decides who can lose data when.

flowchart LR
  App["App"] --> CA["Cache-aside: app writes DB, deletes cache"]
  App --> WT["Write-through: app -> cache -> DB (sync)"]
  App --> WB["Write-behind: app -> cache (return) ; cache -> DB (async)"]

6. Invalidation patterns

When data changes, you need to refresh the cache. Three approaches:

  1. Explicit invalidation: on every write, cache.delete(key). Simple, prone to bugs if any code path forgets to invalidate.
  2. TTL-based: set a time-to-live; cache entries expire automatically. Always works; staleness ≤ TTL. Picks TTL is a balance between freshness and hit rate.
  3. Version keys: include a version in the cache key (user:42:v17). When data changes, bump the version. Old entries age out naturally. Great for content distribution; no manual deletes needed.

Most real systems use a mix: short TTL as the safety net + explicit invalidation on writes for promptness.

7. Thundering herd, dogpile, stampede

A popular cache entry expires. Twenty thousand requests hit the cache at once. All twenty thousand miss. All twenty thousand pile onto the database simultaneously. The DB folds.

Mitigations:

  • Stale-while-revalidate: serve the old value while a background job refreshes it. Users see slightly stale data; the DB sees one refresh, not 20K.
  • Probabilistic early expiration: refresh the cache before the TTL ends, with growing probability as expiration approaches. Spreads load.
  • Single-flight (request coalescing): when N requests miss the same key, only one talks to the DB; the others wait for its answer. Trivial to add in front of any read-through cache.
  • Background refresh: hot keys are refreshed on a schedule, never expire under load.

Default to single-flight + small jitter on TTL. Most stampede problems disappear.

8. What you should never cache

Caching is not free. Don't cache when:

  • Per-user, per-request data. Yes, you can cache it. But the hit rate is ~0% and you're paying memory for no win.
  • Data that must be exactly current: financial balances, inventory in flash sales, anything where a stale read causes real harm.
  • Writes that must be durable on ack: tournaments, ID generation, money. Use a proper transactional store.
  • Things you can recompute trivially: caching len(items) when items is already in memory is just confusion.

The cost of caching is design overhead and stale-data bugs. Apply it where the read pattern is repeated, the source is slow or expensive, and slight staleness is tolerable. That's where caching pays off; everywhere else, it's overhead.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. In cache-aside, what's the most common bug that causes stale data?
    • Setting too long a TTL
    • Forgetting to invalidate the cache after a database write
    • Using JSON instead of binary serialisation
    • Caching too many keys at once
  2. Why is write-behind risky for financial transactions?
    • It's slow
    • Cache failure between ack and DB write means acknowledged data is lost
    • It can't handle string values
    • It requires too much memory
  3. A popular cache entry expires and 20K simultaneous requests hammer the database. What's the cleanest mitigation?
    • Increase the database's connection pool size
    • Use single-flight (request coalescing) so only one request rebuilds the cache while others wait
    • Reduce the TTL to refresh more often
    • Disable caching for that key
  4. Which caching scenario is genuinely *not worth* caching?
    • Article body that thousands of users read per minute
    • Per-user dashboard data that's unique per request and rarely re-requested
    • Product catalogue queried on every page load
    • User session lookup by token
  5. What does "stale-while-revalidate" actually do?
    • Refuse stale data and force a fresh fetch
    • Serve the stale cached value immediately while triggering a background refresh
    • Delete stale entries on access
    • Encrypt stale data before serving

Related lessons

Programming
intermediate

Benchmarks That Hold Up, and Knowing When to Stop

A benchmark is an experiment, and most are badly designed enough to produce confident wrong answers. This lesson covers what a measurement must control to mean anything, the ways microbenchmarks lie including code the compiler deletes, how to catch regressions in continuous integration despite noisy machines, and how to recognise the point where optimising stops paying.

7 steps·~11 min
Programming
intermediate

Where Time Actually Goes: The Six Usual Suspects

Slow software is slow for a short list of reasons, and each one has a signature you can recognise before you find the code. This lesson covers the six recurring bottleneck classes, waiting on I/O, chatty queries, allocation pressure, lock contention, memory access patterns and serialisation, with the symptom that identifies each and the fix that actually works.

7 steps·~11 min
Programming
intermediate

How Profilers Work, and How to Read a Flame Graph

A profiler is not a neutral observer: sampling and instrumentation see different things, distort the program in different ways, and answer different questions. This lesson covers how each works, why CPU time and wall-clock time give opposite answers, and how to read a flame graph correctly, including the axis that means nothing and is misread constantly.

7 steps·~11 min
Programming
intermediate

Measure First: The Arithmetic That Decides What to Optimise

Most optimisation effort is spent on code that was never the problem, and the reason is that intuition about where time goes is reliably wrong. This lesson covers why guessing fails, the arithmetic that caps what any optimisation can buy, the difference between latency and throughput, and how to set a target that tells you when to stop.

7 steps·~11 min