Skip to main content

Caching Patterns: Aside, Read-Through, Write-Through, Write-Behind

The Price Changed. Who Tells the Cache?

Last lesson you cached that viral sneaker listing and turned fifty thousand database queries into one. The page flies, the database naps. Then the seller edits the price — 220becomes220 becomes 180 for a flash sale — and your happy little cache becomes a liar. Fifty thousand people are now being served the old price from RAM, fast and wrong, and they'll keep getting it until that thirty-second copy expires.

So a new question lands, and it's the whole of this lesson: once you decide to cache, how does data actually get in and out? When a read misses, who goes and fetches it from the database — your application, or the cache itself? When a write happens, where does it go — into the cache, into the database, both, and when does the database find out? These aren't academic. Each answer is a different wiring of the same three boxes — app, cache, database — and each wiring trades speed for a different flavor of staleness, or durability, or latency.

You'll hear five names thrown around like a menu you pick one dish from: cache-aside, read-through, write-through, write-behind, write-around. That framing is a trap, and untangling it is the first thing we'll do. The truth is cleaner and it's the spine of this whole lesson: there are two independent decisions, one for reads and one for writes, and every real system makes both.

Scope: we're wiring the read and write paths and weighing their trade-offs. What to evict when the cache fills up is the next lesson; how to keep the copy from lying — the price-change problem above — is Cache Invalidation, two lessons out. Here the database stays a black box: we only care where a read or write lands, not how the database serves it.

A system-design infographic titled 'Reads pick one of two. Writes pick one of three. You combine them.', laying out the caching-pattern design space as two axes of small App-Cache-Database flow diagrams. The top row is the READ axis with two panels. Cache-aside: the application asks the cache, and on a miss the application itself reads the database and writes the value back into the cache, so a highlighted arrow loops from app to database to cache; its tag reads 'the app fills it - the default'. Read-through: the application asks only the cache, and on a miss the cache library reads the database and fills itself, so the highlighted fill arrow runs from cache to database; its tag reads 'the cache fills it - no boilerplate'. The bottom row is the WRITE axis with three panels. Write-through: a write goes to the cache and the database together, synchronously, both arrows bold and simultaneous; tag 'fresh, never lost - slower writes'. Write-behind: a write hits the cache and is acknowledged immediately, with a dashed delayed arrow trickling to the database later; tag 'fastest - risks loss on crash'. Write-around: a write goes straight to the database and bypasses the cache entirely, the arrow to the cache crossed out; tag 'no pollution - reads miss at first'. A caption strip along the bottom reads 'reads choose WHO fills the cache; writes choose WHERE the write lands and WHEN the database hears about it.' The palette is dark slate with sky-blue read panels, amber and emerald and red write panels, and the distinguishing arrow in each panel drawn brightest so the whole design space reads at a glance.

Two Decisions, Not Five Patterns

Here's the mental model to hang everything on. The famous "five patterns" aren't five points on one line — they're two separate questions, and you answer both:

Reads — who fills the cache on a miss?

  • Cache-aside — the application fills it.
  • Read-through — the cache library fills it.

Writes — where does the write go, and when does the database hear about it?

  • Write-through — cache and database, together, right now.
  • Write-behind — cache now, database later (async).
  • Write-around — database only, skip the cache.

That's the entire design space: one read strategy × one write strategy. Two choices on the left, three on the right, and you pick one from each column. "Cache-aside" says nothing about how you write; "write-behind" says nothing about how you read. Real systems are combinations — cache-aside paired with write-around is the pragmatic web-app default; read-through paired with write-through is what the big vendor caches ship; cache-aside with write-behind is how you absorb a firehose of writes. You'll see those combos by name shortly.

Why does this framing matter so much? Because the moment you see reads and writes as separate dials, the trade-offs stop being five things to memorize and become two short questions you can reason about on a whiteboard: for reads, do I want the fill logic in my app or in the cache? For writes, how much write latency, staleness, and data-loss risk can I stomach? Answer those and the "pattern" names itself. Let's take the two axes one at a time.

The Read Axis: Who Fills the Cache?

On a read, there are only two outcomes: the key is in the cache (a hit — return it, done, ~1 ms) or it isn't (a miss — someone has to go get it from the database). The two read patterns differ on one thing only: who is that someone.

Cache-aside (lazy loading) — the default. Your application code owns the dance. It asks the cache; on a miss it reads the database itself, writes the value back into the cache, and returns it. The cache sits aside the app as a dumb key-value box that knows nothing about your database — which is exactly why cache-aside works with plain Redis or Memcached and is, by a wide margin, the most common pattern in the wild. It has a property you'll come to love: if the cache dies, the app just reads the database. Every request stays correct, only slower. The cache is an optimization, never a hard dependency — an operational gift when things are on fire.

Cache-aside's costs are the flip side of that simplicity:

  • The miss penalty. A miss is three hops — ask cache, read database, write cache — so a cold key is actually slower than having no cache at all. You bet on that key being read many more times.
  • The cold-start stampede. Right after a deploy or a cache restart, the cache is empty, everything misses, and the database eats a spike of traffic it hasn't seen in a while (that thundering-herd problem gets its own lesson).
  • Staleness, and a subtle race. Because the app fills the cache separately from writing the database, the copy can rot — like our price change. There's even a sneaky race: two servers miss the same key at once and both reload it (double the database work, last writer wins); worse, a reader can fetch an old value, a writer can update the database and clear the cache in between, and then the slow reader writes its stale value back — poisoning the cache until the TTL saves you. Fixing that is the invalidation lesson; for now just know cache-aside leaves the door open.

Read-through. Same three boxes, one thing moved: the app talks to the cache and only the cache, always. On a miss, the cache itself — via a loader you configure once — reads the database, fills itself, and returns the value. Notice what changed: not what happens on a miss (still lazy, still three hops, still only-requested data cached) but who does it. The messy "check cache, else hit DB, else populate" boilerplate leaves your application entirely and lives in one place inside the cache. That's the whole pitch: separation of concerns and one consistent fill path instead of the same three lines copy-pasted at forty call sites. The price is that you need a cache that supports read-through — Ehcache, Hazelcast, Oracle Coherence, NCache, or a read-through layer in front of Redis — so you're leaning on a smarter, more coupled cache. And you still pay the first-read latency for any cold key.

The one-liner: read-through is cache-aside with the fill delegated to the library. Same trade-offs, different owner. Reach for cache-aside by default and when your cache is a plain key-value store; reach for read-through when you'd rather not scatter fill logic across the codebase and your cache can do it for you.

The Write Axis: Where Does the Write Go?

Reads were a two-way choice. Writes are the interesting one, because now durability and freshness are on the table — a botched read is just slow, but a botched write can lose data. Three patterns, and they differ on where the write lands and when the database finds out.

Write-through — cache and database, together, now. A write updates the cache and the database synchronously, in the same operation, and only then acknowledges. The payoff is freshness with no data loss: the copy and the truth move in lockstep, so the very next read is guaranteed a hit and correct, and the database has the data before the client ever hears "ok." The cost is two writes on the critical path, so every write is slower — and you're writing data into the cache that might never be read, wasting memory and write effort on write-heavy, read-rarely keys. It's the natural partner of read-through (the classic vendor combo: everything flows through the cache). One honest caveat: write-through keeps the cache fresh, but it doesn't keep it warm — a restarted cache is still empty and still misses until reads refill it. Freshness and warmth aren't the same thing.

Write-behind (write-back) — cache now, database later. A write updates the cache and is acknowledged immediately; the database update happens afterward, asynchronously, usually batched and coalesced — flush every N milliseconds, and a hundred updates to the same counter collapse into one database write. This is the fastest write there is (only an in-memory hop before the ack) and it's a shock absorber: it soaks up write bursts and smooths a spiky firehose into a trickle the database can handle. It's how you survive write-heavy workloads — counters, likes, view counts, metrics, leaderboards.

And it's where the danger lives, so say it plainly: an acknowledged write that hasn't flushed yet exists only in the cache. If that cache node crashes before the flush, the data is gone — the database never heard about it, and you already told the user "saved." Write-behind trades durability for speed. It's also eventually consistent: a reader who bypasses the cache and hits the database directly sees the old value during the flush window. You blunt the risk — replicate the cache, back it with a durable log (a WAL, Redis AOF, even Kafka as the write buffer from the last chapter), shorten the flush interval — but you never make the window exactly zero. That's the deal. (You'll feel this in the lab in a second: pick write-behind, crash the cache, watch writes vanish.)

Write-around — straight to the database, skip the cache. The write goes only to the database; the cache isn't touched. The value lands in the cache later, lazily, on the next read. You get the lowest-latency safe write (one write, to durable storage, no loss) and — the real point — you don't pollute the cache with data nobody's about to read. The cost: a read right after a write misses (the value isn't cached yet), and if a stale copy was already cached and you didn't clear it, a read can serve the old value — so write-around is usually paired with clear-the-key-on-write. Its home is write-once, read-rarely data: log and event ingestion, audit trails, bulk imports, video and media upload pipelines — floods of writes that would just evict your genuinely hot keys if you cached them. Which is exactly why write-around pairs so naturally with cache-aside.

Lab: Wire It Yourself

Reading about flows is one thing; watching a request actually travel — and watching a write vanish — is another. Here's the live wiring. Three boxes: your app, the cache, the database. You own two dials: the read pattern (cache-aside or read-through) and the write pattern (through, behind, or around). Set them, then fire reads and writes and watch where each request goes.

Try this in order:

  1. Leave it on cache-aside + write-around — the pragmatic default. Fire a few reads: watch misses trudge to the database and fill the cache, then hits snap back from memory. Fire a write: see it slip past the cache, straight to the database.
  2. Switch writes to write-through and write again — now the write hits cache and database together, and the next read is instantly fresh. Notice the write took longer.
  3. Now the money moment. Switch writes to write-behind and fire a burst of writes — they ack instantly, landing in the cache, with the database updates still queued. Then hit "💥 crash the cache." Every write that hadn't flushed is gone; the LOST WRITES counter climbs. Flip back to write-through or write-around, crash again — zero lost. That gap is the trade-off.

Keep an eye on the three dials as you go — write latency, staleness window, durability. There's no row that wins on all three; every pattern is a different corner of the same triangle. That's the lesson you can't get from prose: you don't pick the "best" pattern, you pick which trade you can live with.

The Caching Pattern Lab. Pick a read pattern (cache-aside or read-through) and a write pattern (write-through, write-behind, or write-around), then fire reads and writes and watch the request actually travel: a hit bounces back from the cache, a miss trudges on to the database and fills the cache on the way home, and a write lands wherever the pattern sends it. Three live dials track the trade you just made - write latency, read-staleness window, and durability. Then hit 'crash the cache': with write-behind selected, every write that hadn't flushed yet is gone and a LOST WRITES counter ticks up - the visceral proof that write-behind buys its speed with durability, while the other write patterns lose nothing.

How They Combine in Real Systems

Because reads and writes are separate dials, the patterns you actually ship are pairs. Four combinations cover almost everything:

  • Cache-aside + write-around — the pragmatic default for read-heavy web apps. A plain Redis or Memcached beside the app; the cache is a pure optimization; the database is the source of truth; writes bypass the cache and clear the stale key so the next read reloads it. Simple, resilient, hard to get badly wrong. Start here.
  • Read-through + write-through — the enterprise/vendor combo (Coherence, NCache, Ehcache, Hazelcast). Everything flows through the cache; the app never touches the database directly; reads are auto-filled and writes are always fresh. You pay write latency and buy strong freshness with almost no app-side cache code.
  • Cache-aside (or read-through) + write-behind — the write-heavy absorber. Counters, metrics, likes, leaderboards: ack writes at memory speed, batch them down to the database, and accept a bounded data-loss window in exchange for surviving the firehose.
  • Cache-aside + write-through — read-heavy and consistency-sensitive on a hot set: simple reads, always-fresh writes, at the cost of slower writes. A middle path when a little write latency beats any staleness.

One bonus pattern, so you recognize the name: refresh-ahead. For a handful of very hot keys with a hard TTL, the cache proactively reloads an entry from the database just before it expires, in the background — so the hot key never actually experiences the miss-and-reload stall that the first request after expiry would normally eat. It's a real feature in enterprise caches, and its own little trade-off: refresh too eagerly and you burn database load on keys nobody asked for; too lazily and misses slip through. Not one of the big five — a nice tool for the rare, predictably-hot key.

Which One Do I Reach For?

You won't derive the pattern from first principles at 2 a.m. — you'll pattern-match. Here's the chooser, reads first, then writes:

Reads:

  • Plain key-value cache, want full control, fine writing a few lines of fill logic → cache-aside (the default — when unsure, this).
  • Want the fill logic out of your app, and your cache supports it → read-through.

Writes:

  • Reads must never be stale and you can pay the write latency → write-through.
  • Write-heavy or bursty, speed matters more than a small loss window → write-behind (and back it with a durable buffer).
  • Write-once, read-rarely — logs, media, bulk loads → write-around (don't pollute the cache).
  • A few very hot keys with a sharp TTL cliff → add refresh-ahead on top.

And the honest meta-answer a staff engineer gives in an interview: "cache-aside plus write-around, invalidate on write, until a number forces me off it." That default is right for the overwhelming majority of read-heavy systems. You move off it only when a specific pressure shows up — write latency you can't afford (→ write-behind), a freshness guarantee you must make (→ write-through), a firehose of unread writes (→ write-around, already there). Name the pressure first; the pattern follows. Reaching for the exotic wiring before you have the pressure is how you buy complexity you didn't need.

Traps That Sound Right

A few of these feel obviously true and quietly wreck a design review:

  • "These five are alternatives — pick one." → No. Reads pick one of two, writes pick one of three, and you combine. "Which pattern?" is really two questions wearing a trench coat.
  • "Write-behind is just a faster write-through." → No — it's faster because it defers durability. The speed and the data-loss risk are the same coin; you can't take one without the other.
  • "Write-through keeps my reads fast." → It keeps them fresh, not warm. A cold cache still misses after a restart. Freshness (correct copy) and warmth (copy present) are different problems.
  • "Read-through does something cache-aside can't." → Same behavior, same trade-offs — it just moves the fill code from your app into the cache. It's an ownership choice, not a capability.
  • "Caching every write is obviously good." → Write-around exists precisely because caching write-once data evicts the hot keys you actually wanted in memory. Sometimes the right move is to not cache the write.

Key Takeaways & What's Next

  • It's two decisions, not five patterns. Reads choose who fills the cache — the app (cache-aside, the default) or the cache library (read-through). Writes choose where the write goes and when the database hears — cache+DB now (write-through), cache now/DB later (write-behind), or DB-only (write-around). You pick one from each column.
  • Cache-aside is the default for a reason: plain key-value cache, and if it dies the app still reads the database correctly. Its costs are the miss penalty, cold-start spikes, and staleness you must manage.
  • The write axis is where durability lives. Write-through = fresh, no loss, slower writes. Write-behind = fastest and burst-absorbing, but an un-flushed write dies with the cache node. Write-around = safe, low-latency, no cache pollution, but reads miss right after a write.
  • Ship combinations. Cache-aside + write-around is the pragmatic default; read-through + write-through is the vendor combo; add write-behind when writes are the bottleneck. Name the pressure before you name the pattern.

Next — Cache Eviction Policies: every pattern here assumes the cache has room. It won't. RAM is finite and your keyspace isn't, so when the cache fills, something has to go — and which something you throw out (LRU, LFU, TTL, and friends) quietly decides your hit rate, which, as you saw last lesson, is the whole game.