Cache Strategy & Invalidation Expert
Design and implement caching architectures using Redis, application-level caching, and CDN layers with reliable invalidation strategies.
Activation Triggers
Activate on: "caching", "Redis cache", "cache invalidation", "cache-aside", "write-through", "TTL", "stale-while-revalidate", "cache stampede", "cache warming"
NOT for: CDN/proxy configuration → api-gateway-reverse-proxy-expert | Database query tuning → data-warehouse-optimizer | Connection pooling → database-connection-pool-manager
Quick Start
- Identify cache candidates — read-heavy, expensive to compute, tolerant of staleness
- Choose pattern — cache-aside (most common), write-through (consistency), write-behind (performance)
- Set TTL strategy — short TTL for volatile data (60s), long TTL + event invalidation for stable data
- Prevent stampede — use probabilistic early refresh or mutex lock on cache miss
- Monitor hit rate — target >90% hit rate; below 70% means your cache strategy is wrong
Core Capabilities
| Domain | Technologies |
|---|---|
| In-Memory | Redis 7.4+, Valkey, DragonflyDB, KeyDB |
| Application | Node LRU cache, Cacheable, unstorage |
| HTTP/CDN | Cache-Control, stale-while-revalidate, Surrogate-Key |
| Multi-Layer | L1 (in-process) → L2 (Redis) → L3 (CDN) |
| Invalidation | Event-driven purge, TTL, tag-based (Surrogate-Key) |
Architecture Patterns
Cache-Aside with Stampede Prevention
import { Redis } from 'ioredis';
const redis = new Redis();
const LOCK_TTL = 5; // seconds
async function cacheAside<T>(
key: string,
ttl: number,
fetcher: () => Promise<T>
): Promise<T> {
// Try cache first
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
// Acquire lock to prevent stampede
const lockKey = `lock:${key}`;
const acquired = await redis.set(lockKey, '1', 'EX', LOCK_TTL, 'NX');
if (!acquired) {
// Another process is fetching — wait and retry
await new Promise(r => setTimeout(r, 100));
return cacheAside(key, ttl, fetcher);
}
try {
const data = await fetcher();
await redis.set(key, JSON.stringify(data), 'EX', ttl);
return data;
} finally {
await redis.del(lockKey);
}
}
Multi-Layer Cache Architecture
Request → L1: In-Process (LRU, 100ms TTL, ~1000 items)
│ miss
↓
L2: Redis (5-60min TTL, shared across instances)
│ miss
↓
L3: CDN (Cache-Control headers, edge-cached)
│ miss
↓
Origin (database/API)
Invalidation flows BACKWARD:
Database change → Purge L2 (Redis DEL) → L1 expires via short TTL
→ Purge L3 (Surrogate-Key purge / CDN API)
Event-Driven Invalidation
// On data change, publish invalidation event
async function updateUser(userId: string, data: UserUpdate) {
await db.users.update(userId, data);
// Invalidate all cache layers
await redis.del(`user:${userId}`);
await redis.del(`user:${userId}:profile`);
// Publish for other instances' L1 caches
await redis.publish('cache:invalidate', JSON.stringify({
pattern: `user:${userId}:*`,
timestamp: Date.now(),
}));
// CDN purge by surrogate key
await cdn.purgeTag(`user-${userId}`);
}
Anti-Patterns
- Cache everything — caching write-heavy, rarely-read data wastes memory and creates invalidation headaches
- No TTL — every cache entry must expire; relying solely on explicit invalidation will eventually leave stale data
- Cache-then-forget — monitor hit rate, eviction rate, and memory usage; an unmonitored cache silently degrades
- Serializing full objects — cache only what you need; storing entire ORM models bloats memory and couples cache to schema
- Invalidation by pattern scan —
KEYS user:*blocks Redis; use sets to track related keys or hash structures
Quality Checklist
- Cache-aside pattern with stampede prevention (mutex or probabilistic refresh)
- TTL set on every key (no eternal cache entries)
- Hit rate monitored and >90% for primary caches
- Invalidation strategy defined: TTL-based, event-driven, or hybrid
- Cache keys include all relevant context (tenant, locale, version)
- Redis memory policy set (allkeys-lru for cache workloads)
- No
KEYS *orSCANin hot paths (use sets for key tracking) - Multi-layer cache with clear TTL hierarchy (L1 < L2 < L3)
- Graceful degradation: app works (slower) if cache is down
- Cache warming strategy for cold starts after deployments