Adding a cache is easy: choose a key, store a value, and return it before querying the slower source. The difficult part begins when the source changes. The application now owns two representations of truth and must define how long they may disagree.
Start with the staleness budget
Ask how wrong the value may be and for how long. Product catalog text might tolerate minutes. Authorization and account balance data may not. A time-to-live is a business consistency decision expressed as a number, not a generic performance setting.
Cache-aside works well when misses are acceptable: read the cache, load from the source on a miss, then populate. It also allows a thundering herd when a popular key expires. Request coalescing, jittered expirations, and stale-while-revalidate can keep one expiration from becoming a database incident.
const cached = await cache.get(key)
if (cached) return cached
return singleFlight(key, async () => {
const value = await database.load(id)
await cache.set(key, value, { ttl: withJitter(300) })
return value
})
Keys are part of the data model
A key must include every input that changes the result: tenant, locale, permissions, query version, and relevant feature flags. Omitting one dimension can leak data or serve a structurally incompatible response. Versioned key prefixes make schema changes and bulk invalidation safer.
Invalidate from committed change
Deleting a cache entry before a database transaction commits creates a race where another request repopulates old data. Invalidate after the source of truth confirms the write, often through an outbox or change event. Consumers should tolerate duplicate invalidation messages because deletion is naturally idempotent.
For some data, updating the cache directly is worthwhile. For others, deletion is safer because the next reader rebuilds from authoritative state. The choice depends on write volume, read latency, and how much coordination the system can reliably maintain.
Design for cache loss
A cache should improve the system, not become an undocumented source of truth. Test cold starts and full eviction. Protect the database with concurrency limits and gradual warming. Monitor hit ratio alongside latency, eviction rate, memory pressure, and source load.
The useful design document for a cache names the source of truth, key shape, staleness budget, population strategy, invalidation trigger, and cold-cache behavior. Once those are explicit, choosing Redis, an in-process LRU, or a CDN becomes the smaller decision it should be.