The pattern in a minute
With cache-aside, the application checks the cache first. On a miss, it reads the source database, stores a copy in the cache, and returns the value.
The cache is an optimization. The source database remains responsible for the authoritative record.
Follow the read
key = "product:v1:" + product_id
cached = cache.get(key)
if cached exists:
return decode(cached)
product = database.find_product(product_id)
if product exists:
cache.set(key, encode(product), ttl=300)
return product
This is pseudocode, not a particular client library. The version in the key helps distinguish serialization or schema changes. A time-to-live bounds how long an untouched entry can remain.
Decide what happens on a write
A common approach updates the source database first, then invalidates the cache key. The next read repopulates it.
That sequence still has races. A reader may fetch an old database value before a writer commits, then place that old value in the cache after the writer invalidates it. A TTL limits the duration but does not make the read strongly consistent.
For data where freshness is essential, consider bypassing the cache, checking a version, or designing stronger coordination around the specific access pattern.
Prepare for a missing cache
Use bounded connection and operation timeouts. If the cache is unavailable, a controlled fallback can keep the application working, but the source database must be able to absorb the load.
A sudden wave of misses can cause a stampede. Request coalescing, bounded locks, and randomized expiration can reduce repeated work. Each introduces its own failure handling.
Measure the whole path
Track hit rate alongside source-database load, request latency, stale-read tolerance, and error rates. A high hit rate does not prove the cached data is correct.
Before implementing this pattern, write down one answer: how stale may this particular value be? That constraint should determine the design.