The basic cache-aside flow in my system design notes is easy to draw: read the cache, fall back to the database on a miss, and save the result for next time. On a write, update the database and invalidate the cached value.
The interesting part begins when two requests overlap. Deleting an entry does not stop another request from putting an older value back.
Follow one delayed reader
Here is a thought experiment involving a product description. The database initially contains version one, and the cache is empty.
- A reader misses the cache and obtains version one from the database.
- Before that reader fills the cache, a writer commits version two.
- The writer deletes the cache key and finishes.
- The delayed reader stores its earlier result: version one.
Every individual operation can succeed, yet the cache ends up stale. The writer used the sensible database-first ordering; a concurrent cache fill still crossed that boundary.
This is why I read “invalidate after writing” as a useful default, not a consistency guarantee. Microsoft’s cache-aside guidance explicitly notes that the pattern does not guarantee consistency between the cache and the data store.
A TTL needs a starting point
A time to live gives an entry an expiry policy. It does not establish that the value was current when it entered the cache.
In this example, the delayed reader can attach a fresh TTL to an old value. If I want to promise a maximum delay after a database change, I need to account for that read-and-fill interval too. Repeated stale fills or a lagging source can complicate the promise further.
My first design decision would be about the product requirement. A briefly outdated description may be acceptable. A decision to reserve the last unit of stock needs an authoritative operation with appropriate concurrency control. I would not let a convenient display cache make that decision.
An expired hot key creates a different problem
Now imagine that the cached description is very popular. When it expires, many requests can miss together and all ask the database for the same value.
Even if every request receives a perfectly current answer, the database can still be overwhelmed. This is a load problem rather than the stale-fill race above.
Request coalescing is one possible response: let one in-flight request fetch a key while others await its result. Its scope matters. A lock inside one application instance does not coordinate ten other instances. I would also define what happens if that fetch times out, so waiting requests do not accumulate indefinitely.
These are choices to evaluate for the workload, not features that appear automatically because a cache has a TTL.
Turn the diagram into a test plan
I would test the stale-fill case by pausing a reader after its database read, allowing a writer to commit and invalidate, then resuming the reader. For the load case, I would expire a hot key and measure how many database reads a burst of requests produces.
Those tests ask different questions. One checks freshness; the other checks how the system behaves under a miss. Keeping them separate makes the cache easier to reason about—and stops “we added Redis” from standing in for a complete design.
Adapted with AI assistance from my caching study notes. The interleavings and test plans are explanatory examples; no Redis benchmark or concurrency test was executed for this article.