ENGINEERING NOTES
Redis Caching: Cache-Aside, TTL and Invalidation
Treat cached data as disposable acceleration and make stale reads, failure and invalidation policies explicit.
What problem does this solve?
Repeated public reads can consume database capacity even when content changes rarely. Caching helps only if correctness tolerates the chosen freshness policy and the cache does not become an undocumented second source of truth.
What is it?
Cache-aside checks a cache first, loads the authoritative database on a miss, then stores the result with a TTL. Writes update the database and invalidate or version affected entries.
Mental model
A cache is a disposable photocopy with an expiry date. The database keeps the original. Losing photocopies should make reads slower, while losing the original is a different class of incident.
How it works internally
A key represents a specific query and scope. Serialization must preserve the expected response shape. A TTL bounds one form of staleness but does not solve races: a reader can load old data before a write, then repopulate it after invalidation. Generation keys or version checks can address that race. Many simultaneous misses can create a cache stampede.
Production example
Public published-content reads may tolerate short staleness; permission and draft visibility checks generally cannot. Include tenant and visibility scope in cache keys, and make database predicates authoritative even during cache failures.
Node.js / TypeScript implementation
An adapter sketch with explicit cache deadlines and runtime parsing. It demonstrates best-effort cache behavior; generation-based invalidation and single-flight suppression are intentionally separate requirements.
type Cache = {
get(key: string): Promise<string | null>;
set(key: string, value: string, ttlSeconds: number): Promise<void>;
};
async function cached<T>(
cache: Cache, key: string, load: () => Promise<T>,
parse: (raw: string) => T
): Promise<T> {
try {
const hit = await cache.get(key);
if (hit !== null) return parse(hit);
} catch { /* Read timeout or invalid cache: use the database. */ }
const value = await load();
try { await cache.set(key, JSON.stringify(value), 60); }
catch { /* Cache failure must not fail a successful database read. */ }
return value;
}Common mistakes
Do not cache authorization-sensitive data under a shared global key. Do not assume deletion after a write prevents every stale repopulation. JSON.parse alone does not validate the resulting shape. Silent cache failures need metrics even if requests succeed.
Performance / reliability considerations
Set client deadlines so cache fallback is actually reachable. Bound TTL and entry size, consider jitter for synchronized expiration, and monitor hit rate alongside database load. Cache outages can move traffic abruptly to PostgreSQL; fallback must fit available capacity.
Interview questions
What happens on a miss or Redis outage? Can stale data reappear after invalidation? What causes a stampede? Which key fields prevent cross-tenant leaks? Why can a higher hit rate still hide a bad cache design?
Quick Revision
Database first for truth; cache for acceleration. Key by query, tenant and visibility. Use TTL, runtime validation and bounded cache calls. Invalidate or version writes. Plan for stale repopulation and stampedes. Fallback increases database load. Measure failures and freshness, not only hit rate.