← Concepts

阅读中文版本

Cache-Aside

Cache-aside loads data into the cache on demand, keeps the database authoritative, and requires explicit invalidation and stampede protection.

caching

Definition

Cache-aside (lazy loading) is a caching pattern in which the application checks the cache first, reads from the database on a miss, and writes the result back to the cache. The application owns both the read path and the invalidation logic; the cache does not automatically load from the database.

Context and scope

Cache-aside is common for read-heavy data that is expensive to compute or query, such as product pages, user profiles, and rendered fragments. It works well when the application can tolerate a cache miss and when invalidation can be tied to writes.

Common misconceptions

  • Cache-aside keeps the cache and database consistent automatically — the application must invalidate or update the cache on writes
  • A long TTL removes the need for invalidation — it only bounds how long stale data can persist
  • A miss is cheap — a hot key miss can stampede the database without request coalescing

Anti-patterns

  • Writing the database and deleting the cache before the transaction commits, so a concurrent reader repopulates stale data
  • Serving cached data after a delete because invalidation was never implemented
  • Using cache-aside for data that must be strongly consistent across readers

Related concepts

Related examples

Authoritative source

https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside

The read path

A cache-aside read checks the cache, returns a hit, or loads from the database and populates the cache on a miss. The database remains the source of truth. The application decides what to cache, for how long, and how to invalidate.

async function getUser(id: string): Promise<User> {
  const key = `user:${id}`;
  const cached = await cache.get(key);
  if (cached) return JSON.parse(cached);

  const user = await db.findUser(id);
  if (user) await cache.set(key, JSON.stringify(user), { EX: 300 });
  return user;
}

Set a TTL even when you invalidate explicitly. The TTL is a safety net for missed invalidation, deploy bugs, and manual database changes. A cache with no TTL can serve stale data forever after one missed delete.

The write path is the hard part

The common recommendation is to update the database, commit, then delete the cache key. Deleting after commit avoids the race where a reader repopulates the cache from the old value before the transaction commits. Even then, a concurrent reader can load the old value just before the commit and write it back after the delete. A short TTL and versioned keys reduce the window; a strict consistency requirement needs a different design.

Do not update the cache and database in a dual write without a plan for partial failure. If the cache update succeeds and the database write fails, the cache holds a value that never existed. Delete-after-commit plus TTL is simpler to reason about than trying to keep two writes atomic.

Protect hot keys

A popular key that expires can produce a stampede. Coalesce concurrent misses with single-flight, serve stale while revalidating, or add TTL jitter. Negative caching for missing records should be short; otherwise a newly created record appears absent.

When to use it

Use cache-aside for read-heavy data where a miss is acceptable and the application can invalidate on writes. The advantage is simplicity and control; the drawback is that every read path must implement the pattern and every write path must remember invalidation. Compared to read-through caching, cache-aside keeps the cache product independent of the database but puts more logic in the application.