A cache has no opinion about your data. It stores what it’s given and returns what it has.
Somebody has to decide who puts data in, who updates it when the database changes, and what happens when the two disagree. That decision is the caching strategy, and it matters more than which cache product you pick.
This is Part 2 of the caching series. Part 1 covered why caches work and the hit-ratio math.
In one minute: Start with cache-aside. It’s the simplest, it survives cache failures, and it covers most needs. Reach for write-through when readers must see fresh data right after a write. Use write-behind only when you can afford to lose writes.
Two Questions Decide Everything
Every strategy answers two questions:
- On a read miss, who fetches the data? Your application, or the cache itself?
- On a write, where does it go first? The database, the cache, or both?
Five combinations come out of that, and each has a name.
| Strategy | Reads | Writes |
|---|---|---|
| Cache-aside | App checks cache, falls back to DB, fills cache | App writes DB, then invalidates cache |
| Read-through | Cache fetches from DB on a miss | (pairs with any write strategy) |
| Write-through | Write goes to cache and DB, in the same operation | |
| Write-behind | Write goes to cache; DB is updated later | |
| Write-around | Write goes straight to DB, skipping the cache |
1. Cache-Aside (Lazy Loading)
This is the one you’ll use most. The application owns the logic. Here’s a read that misses, step by step:
- The app checks the cache first.
- Miss. The cache has nothing for this key.
- So the app goes to the database itself.
- The database answers ₹499. This is the slow part.
- The app stores the result in the cache, so the next read is fast.
And in code:
async function getProduct(id: string): Promise<Product> {
const cached = await cache.get(`product:${id}`);
if (cached) return cached; // hit
const product = await db.products.findById(id); // miss: go to the source
await cache.set(`product:${id}`, product, { ttl: 300 });
return product;
}
async function updateProduct(id: string, changes: Partial<Product>) {
await db.products.update(id, changes);
await cache.delete(`product:${id}`); // next read refills it
}
Why people like it
- Only data that’s actually requested gets cached. No wasted memory.
- If the cache dies, your app still works. It just gets slower, because every request becomes a miss.
Where it hurts
- The first request for anything is slow (a cold miss).
- There’s a window between a database change and the cache catching up where readers see old data.
- Every code path that touches the data has to remember the cache logic. Forget one, and you get bugs that only show up in production.
Notice that on update we delete the cache entry instead of overwriting it. That feels odd the first time you see it, and the reason is a nasty race condition that gets its own post: Part 4.
Uber’s integrated cache, CacheFront, follows exactly this read path. Check Redis, fetch the missing rows from storage, and populate Redis on the way back (Uber engineering).
2. Read-Through
Same idea as cache-aside, except the cache is responsible for loading data. Your code just asks the cache. If the cache doesn’t have it, the cache goes and gets it. Compare the arrows with the diagram above: the app no longer has a line to the database.
- The app asks the cache. It never talks to the database.
- Miss, so the cache itself goes to the database.
- The database answers ₹499 and the cache keeps a copy.
- The cache hands the value to the app.
const product = await cache.get(`product:${id}`); // cache loads from DB on a miss
The upside is a cleaner application: one API, no cache logic scattered around.
The downside is subtler. When the cache sits in front of the database, it becomes part of your availability story. Amazon’s Builders’ Library makes this point: inline caches give clients a uniform API, but they make the cache part of the availability equation, whereas side caches (cache-aside) let you degrade gracefully when the cache is unhealthy (source).
Neither is wrong. Just know which one you chose.
3. Write-Through
Every write goes to the cache and the database, and the write isn’t done until both succeed.
- The app writes ₹599 to the cache.
- The cache writes it through to the database, and waits.
- The database confirms the write.
- Only now does the app hear "saved". Both stores agree. The price was slower writes.
Why people like it
- The cache is always fresh. A read right after a write sees the new value.
- Simple mental model: the cache can’t be older than the database (barring failures).
Where it hurts
- Writes are slower, since they pay for two stores.
- You cache data that may never be read. If most of what you write is never requested again, you’re filling memory with junk.
A real example: Amazon DynamoDB Accelerator (DAX) is documented as a write-through cache. It passes your writes through to DynamoDB synchronously and updates its own item cache after (AWS docs).
4. Write-Behind (Write-Back)
The write goes to the cache and returns immediately. The database gets updated later, in the background, often in batches.
- The app writes ₹599 to the cache.
- The cache replies "saved" straight away. The user is happy and the write was fast.
- Later, in the background, the cache flushes the change to the database. (dashed arrow: happens later, in the background)
CPUs do this. A write-back CPU cache marks a line “dirty” and writes it to memory only when it’s evicted. If you remember that from a computer architecture class, it’s the same concept.
Why people like it
- Writes are very fast.
- Many updates to the same key can collapse into one database write. Great for counters.
Where it hurts
- If the cache dies before it flushes, those writes are gone. The user got a success response for data that never reached the database.
- Ordering and retry logic get tricky.
- The database is behind. Anything reading the database directly (reports, other services) sees old data.
Think of a shopping cart. Write-behind makes adding items feel instant, and losing the last few clicks in a crash is annoying but survivable. Now think of a payment ledger. Same strategy, completely different answer: never.
5. Write-Around
Writes go straight to the database. The cache is skipped, and gets filled later only if someone reads the data.
- The app writes straight to the database and skips the cache.
- The cache was not touched. It holds nothing for this item, and nothing it already had gets pushed out.
- Later, the first read checks the cache and misses. From here it behaves like cache-aside.
When it fits: data that is written once and read rarely or never. Think uploaded files, log entries, audit records. Writing them into the cache would push out data that people actually read.
The catch: the first read after a write is always a miss. And if you’re updating existing data rather than creating new data, you still need to invalidate the old cached copy.
Play With It
Reading about arrows only goes so far. Below, pick a strategy and step through it. Watch the cache and database boxes as you go: the interesting moments are when they disagree.
Two things worth trying: on cache-aside, step through Write and notice the moment the cache holds a stale price. On write-behind, pick cache crashes first and see what the user was told versus what the database actually has.
Choosing: A Practical Table
| What you’re storing | Pick | Why |
|---|---|---|
| User profile, product details | Cache-aside | Read-heavy, a few seconds of staleness is fine |
| Data other services rely on being fresh right after a write | Write-through | The cache can’t lag |
| Shopping cart, session state | Write-through, or write-behind if you accept some loss | Speed vs durability |
| View counters, “likes”, analytics | Write-behind | Many small writes, tiny losses acceptable |
| Payments, balances, anything transactional | Don’t use write-behind. Probably don’t cache the source of truth at all | See my post on Transactions |
| Audit logs, uploads | Write-around | Written once, rarely read |
In the Wild: When Cache-Aside Wasn’t Enough
For years Facebook used memcache as a lookaside cache in front of MySQL for its social graph, which is cache-aside at massive scale. Every client had to implement the cache logic correctly.
Then they built TAO, a data store that sits between web servers and MySQL, understands the graph (objects and associations), and manages its own cache. In the TAO paper they describe this as avoiding some fundamental shortcomings of a lookaside cache architecture. In their sampled workload, reads were 99.8% of API requests with an overall read hit rate of 96.4% (TAO, USENIX ATC 2013).
My read on it: cache-aside is a perfectly good start, and at a big enough scale, with enough teams touching the same data, you may want to push the cache logic out of every client and into a shared layer. That’s a scale problem, not a day-one problem.
My Take
If you’re picking a strategy and feeling unsure, the order I’d go in:
- Cache-aside with a TTL. It works, it fails safe, and everyone on your team will understand it.
- Add explicit invalidation on writes so you’re not waiting for TTLs.
- Only then ask whether any specific path needs write-through or write-behind.
The strategies that make writes faster are the ones that make failure harder to reason about. Don’t pay that price until you’ve measured a reason to.
Try It Yourself
In Cache Lab, scenario 2 runs the same workload under all five strategies, side by side.
Challenge: find the strategy that survives a cache crash with zero lost writes, then find the one that gives the lowest write latency. They won’t be the same one.
Summary
How a cache connects to its database comes down to two questions: who loads on a miss, and where do writes go. Cache-aside is the sensible default. Read-through hides the logic but ties the cache into your availability. Write-through keeps things fresh at the cost of write speed. Write-behind is fast and risky. Write-around protects the cache from data nobody reads.
None of them solves staleness completely, which is where this series is headed.
Next up: Who Gets Evicted? LRU, LFU, and the Algorithms Inside Real Caches. The cache is full. What gets thrown out?
References
- Nishtala et al. Scaling Memcache at Facebook, NSDI 2013
- Bronson et al. TAO: Facebook’s Distributed Data Store for the Social Graph, USENIX ATC 2013
- DAX and DynamoDB consistency models, AWS documentation
- Brinkley, Chhabra. Caching challenges and strategies, Amazon Builders’ Library
- Uber. How Uber Serves Over 40 Million Reads Per Second Using an Integrated Cache