Data building blocks
Cache patterns
A cache pattern decides who loads data into the cache and when writes reach the database. Cache-aside is the usual choice, because the cache is optional and the app still works without it.
Every cache has to answer two questions. Who fills it on a miss? And when a write happens, does the cache update, or does the database alone take the write? The answers form a few named patterns. They differ in how fresh the data is, how fast writes are, and what breaks when something fails.
Concept
The database is the source of truth. The cache holds a copy. A miss is a read the cache can't answer, so the app goes to the database and fills the cache for next time.
The big idea
Think of a library. In one setup, you walk to the shelves yourself, and if a book isn't on the reserve desk, you fetch it and leave a copy there. In another setup, you ask the desk, and the desk fetches the book for you. Both work. The difference is who owns the fetching logic.
The most common pattern, cache-aside, puts that logic in the app. The cache is optional, so if it's down, the app still works, just slower.
Walking through it
Here is cache-aside. The code shows both the read path and the write path.
// Sketch: `redis` and `db` are stand-ins for a Redis client and a data layer.
async function getProduct(id: string): Promise<Product> {
const key = `product:v3:${id}`
const hit = await redis.get(key)
if (hit !== null) return JSON.parse(hit)
const product = await db.products.findById(id)
const ttl = 300 + Math.floor(Math.random() * 60) // 300–359 s, jittered
await redis.set(key, JSON.stringify(product), "EX", ttl)
return product
}
async function updateProduct(id: string, changes: Partial<Product>) {
await db.products.update(id, changes)
await redis.del(`product:v3:${id}`) // delete, don't set
}The other patterns differ in who does what.
| Pattern | Reads | Writes | Trade-off |
|---|---|---|---|
| Cache-aside | App checks the cache, then the database on a miss | App writes the database, then deletes the key | Simple and resilient. The app owns the logic |
| Read-through | Cache loads from the database on a miss | Usually app writes to the database | Cleaner app code, less control |
| Write-through | Reads come from the cache | Every write goes to cache and database together | Fresh reads, but every write pays twice |
| Write-behind | Reads come from the cache | Writes go to the cache and flush to the database later, often in batches | Very fast. Unflushed data is lost if the cache node dies |
| Refresh-ahead | Popular entries reload before they expire | Usually database first | No misses for hot keys. Wasteful for a long tail |
Write-behind shows the trade-off well. 100 increments of a view counter can become one database UPDATE. But an order that was never flushed is simply gone after a crash. Use it for loss-tolerant counters, not orders.
Common mistakes
- Write-behind for money or orders. Anything not yet flushed is lost if the cache node dies.
- Updating the cache on write instead of deleting. Concurrent writers can leave an old value in the cache. Delete the key, so the next read loads the current value.
- Refresh-ahead on the long tail. You pay to reload keys nobody reads.
Check yourself
- Why is cache-aside the usual default? The cache is optional. If it fails, the app still works, just with more database load.
- What is the main risk of write-behind? Writes that haven't been flushed yet are lost if the cache node fails.
Key takeaways
- Cache-aside is the default: the app checks the cache, loads on a miss, and deletes on write.
- Read-through and write-through move logic into the cache layer, at the cost of control.
- Write-behind is fast but can lose data, so keep it for loss-tolerant counters.
Further learning
The invalidation page explains why deleting on write is safer than setting a new value.
Next, read about hit rate and sizing, which shows how much load a cache actually removes.
Related
Caching
Where to put a cache, how to read and write through it, how to size it from hit-rate math, and how to keep it from serving stale data or melting down.
Cache layers
A request can be served from a cache at many layers, from the browser to the database. The closer a cache sits to the user, the more load it saves and the harder it is to invalidate.
Cache invalidation
A cache learns that its data is stale through TTLs, deletes on write, versioned keys or change-data-capture. Each gives bounded staleness, not strong consistency, so choose by how stale you can tolerate.