A read-heavy endpoint is a tempting place to introduce Redis. But a faster response is only useful when its contents are still appropriate for the user. Before choosing a TTL, decide which data may be stale, who may see it, and how the endpoint behaves when the cache is unavailable.
Choose a narrow first target
A public catalog summary is often a better starting point than an authorization decision or a mutable account balance. Measure repeated reads and database cost, then cache a specific representation. Keep the database as the source of truth.
With cache-aside, the application checks the cache first. On a miss, it reads the database and stores the result with an expiry. The cache can be empty without making the underlying data disappear.
// Illustrative cache-aside flow.
const key = `catalog:v2:${tenantId}:${productId}`;
const cached = await cache.get(key);
if (cached !== null) return JSON.parse(cached);
const product = await loadProduct(tenantId, productId);
await cache.set(key, JSON.stringify(product), { EX: 60 });
return product;The version identifies the cached representation, and the tenant ID prevents two organizations from sharing a key accidentally. For query results, include every parameter that changes the result, including filters, ordering, pagination, and any relevant visibility scope. Validate access independently of a cache hit.
Expiry limits residence time, not every race
After a database update commits, invalidate the affected key. Expiry provides a fallback when invalidation is missed, but it is not a strict promise that readers immediately see the latest value.
A reader can load old data, a writer can update and invalidate, and the reader can then put that old data back into the cache. For workflows where this is unacceptable, consider version checks or bypassing the cache. Name the freshness requirement before adding coordination.
Prepare for a crowded cache miss
If a popular key expires, many requests may reach the database together. Coalescing concurrent loads within a process reduces repeated work there. Across multiple instances, coordination needs a separate design. Slightly varied expiries can reduce synchronized expiration across many keys.
Any lock-based approach needs bounded waiting and a fallback. A cache optimization should not become an indefinite queue.
Keep failure and success measurable
- Track hit rate alongside endpoint latency and database load.
- Set short cache connection and operation timeouts appropriate to the service.
- Decide whether to fall back to the database on cache failure, and protect that fallback from overload.
- Test cross-tenant isolation, write invalidation, and a fully cold cache.
A cache is a tradeoff between repeated work and tolerated staleness.
Start with a small, explicit cache boundary. Expand it only when measurements show that the complexity is buying a useful improvement.