Guide

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.

TypeScript
// 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.

PatternReadsWritesTrade-off
Cache-asideApp checks the cache, then the database on a missApp writes the database, then deletes the keySimple and resilient. The app owns the logic
Read-throughCache loads from the database on a missUsually app writes to the databaseCleaner app code, less control
Write-throughReads come from the cacheEvery write goes to cache and database togetherFresh reads, but every write pays twice
Write-behindReads come from the cacheWrites go to the cache and flush to the database later, often in batchesVery fast. Unflushed data is lost if the cache node dies
Refresh-aheadPopular entries reload before they expireUsually database firstNo 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

  1. Why is cache-aside the usual default? The cache is optional. If it fails, the app still works, just with more database load.
  2. 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.