Knowledge Guide
HomeSystem DesignCaching

hard Cache Write Strategies: write-through, write-back, write-around

A write strategy is two decisions: does the second store get written, and is it synchronous

Every cache-write strategy answers the same pair of questions: when the app writes, does the write touch the cache at all, and if it touches both cache and DB, does the second write happen synchronously (before the caller gets an ack) or asynchronously (after)? Four well-known strategies fall directly out of those two axes — write-through, write-back, write-around, and cache-aside — and every consistency/latency/durability trade-off below is a consequence of which answer you picked.

Four write paths compared: write-through writes cache then synchronously the DB before acking; write-back acks immediately and flushes to DB asynchronously in a batch, risking loss if the cache dies first; write-around writes DB only, bypassing the cache entirely, so the next read is a guaranteed miss; cache-aside writes DB first then invalidates or updates the cache entry, which is safest via invalidate but still open to a concurrent-read stale-repopulate race.
Four write paths compared: write-through writes cache then synchronously the DB before acking; write-back acks immediately and flushes to DB asynchronously in a batch, risking loss if the cache dies first; write-around writes DB only, bypassing the cache entirely, so the next read is a guaranteed miss; cache-aside writes DB first then invalidates or updates the cache entry, which is safest via invalidate but still open to a concurrent-read stale-repopulate race.

Write-through — write both, synchronously, before the ack

The app writes to the cache; the cache forwards the write to the database synchronously and only acks the app once the DB confirms. There is never a window after the ack where cache and DB disagree.

Traced — updating user:42.name from “Sam” to “Samir”:

t (ms)Event
0App calls cache.put(user:42, {name:"Samir"})
0–12Cache forwards the write to the DB and blocks waiting for its commit
12DB commits, acks the cache; cache commits its own copy and acks the app
13A read for user:42 hits the cache and returns “Samir” — read-after-write is guaranteed fresh

Total write latency = cache write + DB write; the cache never speeds up a write, only reads after it. Good fit when reads follow writes almost immediately and staleness is unacceptable (profile pages, account settings, inventory counts read right after checkout).

Write-back / write-behind — ack fast, flush later, batched

The app writes to the cache and gets an immediate ack; the cache buffers the write and flushes it to the DB asynchronously, often batching several writes to the same key into one DB write (write coalescing). This is the fastest write path, but it opens a durability window: the DB does not have the new value until the flush runs.

Traced — three writes to key counter:X inside one flush window:

t (ms)EventDB state
0Write A; cache acks in <1 msstale (old value)
5Write B (same key); cache acks in <1 msstale
9Write C (same key); cache acks in <1 msstale
10Flush timer fires: only C (the latest value) is written to the DB — A and B never touch the DB individuallynow = C

Three writes coalesced into one DB write — that’s the throughput win. But if the cache node crashes at t=7 (after acking A and B, before the t=10 flush), both buffered writes are lost forever — the DB never sees them, and the app already told the user they succeeded. That is the defining risk of write-back: an ack is not a durability guarantee until the flush has actually happened. Replicating the cache buffer reduces the blast radius but does not remove the window.

Write-around — write DB only, bypass the cache entirely

The write goes straight to the DB; the cache is not touched, populated, or invalidated. This avoids filling the cache with data that is written once and rarely (or never) read again — e.g. an audit log row, a bulk import, a cold historical record.

Traced — write order:9001.status = "shipped", then a read arrives:

t (ms)Event
0App writes directly to the DB; cache is never contacted
8DB commits, acks the app
9A read for order:9001 arrives — cache miss (it was never populated), falls through to the DB, loads the row, populates the cache
10Every subsequent read hits the cache

The cost is explicit and guaranteed: the very next read after a write-around write is a miss, paying a full DB round trip. That is the correct trade for data that’s written far more often than it’s read; it is the wrong choice for data that is read again soon after being written.

Cache-aside (lazy loading) — the default in most systems

Read path: the app checks the cache first; on a miss it loads from the DB and populates the cache itself (the cache never loads data on its own). Write path: the app writes to the DB, then either invalidates (deletes) the cache entry or updates it in place — the cache is never the primary target of a write.

Invalidate vs. update — not a cosmetic choice

Invalidate (delete) is the safer default: after the write, the key is simply absent, so the next reader is forced through the normal miss → load-from-DB → populate path and can never see a value older than the write. Update in place (write the new value straight into the cache right after the DB write) saves that next reader a DB round trip — but it reopens a race that invalidate mostly avoids, because it assumes no other read is concurrently in flight for the same key.

The concurrent-read-repopulates-stale race

Even with invalidate, a slow concurrent read can still leave the cache stale. Walk it with real timestamps:

  1. t0 — Reader R1 misses the cache for key K and begins a slow DB read (replica lag, a long query, GC pause — anything that takes a while).
  2. t1 — Writer W writes the new value to the DB, then invalidates (deletes) cache key K. K wasn’t cached yet, so the delete is a no-op.
  3. t2 — R1’s slow read finally returns — but it fetched the value as of before t1, i.e. the OLD value.
  4. t3 — R1, unaware anything changed, populates the cache with that old value. The cache now holds stale data, and there is no pending write left to invalidate it again.

This is why “invalidate on write” is a strong mitigation, not a proof of correctness: a read that started before the write finished can still complete after it and clobber the cache with old data. Mitigations: a short TTL as a backstop (bounds how long the staleness can last), or a version/timestamp check before repopulating (only write to cache if the fetched version is still the latest known).

Timeline: at t0 reader R1 misses the cache and starts a slow DB read. At t1 writer W writes the new value to the DB and invalidates the (empty) cache key. At t2 R1's slow read returns the old value it fetched before the write. At t3 R1 populates the cache with that old value, leaving the cache stale until the next TTL expiry or write. Fix: a short TTL backstop or a version check before repopulating.
Timeline: at t0 reader R1 misses the cache and starts a slow DB read. At t1 writer W writes the new value to the DB and invalidates the (empty) cache key. At t2 R1's slow read returns the old value it fetched before the write. At t3 R1 populates the cache with that old value, leaving the cache stale until the next TTL expiry or write. Fix: a short TTL backstop or a version check before repopulating.

Pitfalls

Judgment — which strategy for which workload

WorkloadPickWhy
Read-heavy, reads follow writes (read-after-write matters)Write-through, or cache-aside with invalidateCache is never allowed to be stale after an ack (write-through), or the very next read is forced to reload fresh (cache-aside + invalidate)
Write-heavy, bursts of writes to the same keys, brief staleness tolerableWrite-backCoalescing collapses N writes into 1 DB write; you are trading a durability window for throughput
Write-once, rarely (or never) re-readWrite-aroundNever pollute the cache with data unlikely to be requested again; pay the one guaranteed miss only if it is actually re-read
General-purpose, mixed read/write, want lazy populationCache-asideOnly caches what is actually read (cold data never enters the cache); most flexible default

Invalidate vs. update, restated as a trade-off: invalidate costs one extra DB round trip on the next read but bounds staleness to “never” in the common case; update-in-place saves that round trip but must be paired with a version check or short TTL if any concurrent-read race is possible — without one, it is not actually safer than doing nothing.

Cache-aside vs. write-through, as named alternatives: write-through guarantees freshness on every write but pays write latency on every write, even for keys that are never read again; cache-aside only pays a load cost for keys that are actually requested, but has a small window (above) where a stale value can be repopulated. Pick write-through when staleness is unacceptable and writes are far less frequent than reads; pick cache-aside when most written keys are read again soon but not so urgently that the race above matters.

Takeaways


Sources: Kleppmann, "Designing Data-Intensive Applications" (ch. 3, 11); AWS "Database Caching Strategies Using Redis" whitepaper; Redis and Memcached documentation on write-through/write-behind; ByteByteGo and Hello Interview system-design caching notes. See also: Cache Stampede & Invalidation, ReadThrough vs WriteThrough Cache, Cache Invalidation, Consistent Hashing. Re-authored/Deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on Cache Write Strategies: write-through, write-back, write-around? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Cache Write Strategies: write-through, write-back, write-around** (System Design) and want to truly understand it. Explain Cache Write Strategies: write-through, write-back, write-around from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Cache Write Strategies: write-through, write-back, write-around** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Cache Write Strategies: write-through, write-back, write-around** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Cache Write Strategies: write-through, write-back, write-around** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes