Redis Cache-Aside: A Practical Guide to Freshness and Invalidation
Adding Redis can reduce repeated database reads, but it also creates a second place where a value can exist. Before introducing a cache, decide how old the value may be and what should happen when Redis is unavailable.
Consider a learning platform with course descriptions and prices. A slightly stale description may be acceptable. A stale price used to charge a customer is a different product problem. These values should not automatically receive the same caching policy.
Follow the basic read path
In a cache-aside design, the application checks the cache, reads the source on a miss, and may cache that result. Redis documents this approach in its cache-aside guide.
Here is framework-independent pseudocode for a public course summary:
key = "course-summary:v1:" + validatedCourseId
cached = tryCacheGetWithShortTimeout(key)
if cached is valid:
return cached
course = database.findPublishedCourse(validatedCourseId)
if course does not exist:
return notFound
tryCacheSetWithExpiry(key, publicSummary(course), ttl)
return publicSummary(course)
The database result remains usable when a best-effort cache write fails. Keep the cache deadline short enough that a degraded Redis service does not make every request wait unnecessarily.
Make cache keys describe the result
A key for a filtered list must include the filters that change its contents. If language, publication state, page cursor, or tenant changes the result, account for it in the key.
Do not put a private account response into a shared public key. Validate identity and authorization before using private cached content. A cache hit is not proof that the current caller may see the value.
Version the key format when the cached schema changes. This avoids treating an old serialized object as a new response contract.
Plan the write path separately
A simple update sequence is: commit the database change, then invalidate the relevant cache key. This is useful, but not a guarantee of immediate consistency.
Imagine a reader loads an old database value just before the writer commits. The writer commits and deletes the key. That reader can then put the old value back. Expiry limits the duration, but does not eliminate the race.
For data requiring stronger freshness, consider bypassing the cache at the critical decision, using versioned data, or designing coordinated invalidation. Choose the complexity based on the consequence of serving a stale value.
For checkout, obtain the price from the authoritative server-side pricing path when creating the order. Do not trust a price displayed earlier in a cached card.
Prepare for popular keys expiring
If many requests miss the same key together, they may all hit the database. One possible mitigation is per-key request coalescing: one loader fetches the value while other requests share its result. A lock needs an expiry and a failure policy; it must not leave callers waiting indefinitely.
Small variation in expiry times can also reduce synchronized expiration across many keys. Neither technique replaces capacity planning for a cold cache or a Redis outage.
Measure the trade-off
Track cache hits, misses, load time, cache errors, and database load. A high hit rate does not prove that the cache is serving correct or useful data.
Test these scenarios before rollout:
- An update occurs while a cache miss is in progress.
- Redis is unavailable during both reads and writes.
- A cached object has an outdated shape.
- Many clients request one expired key.
- A user tries to access another account’s cached data.
Start with a read-heavy, low-risk resource. Write the freshness rule beside the code, so future developers understand why the cache exists and where it must be bypassed.
See the backend learning path for related application-design topics.
