Cache Invalidation Strategies
📖 Concept
"There are only two hard things in CS: cache invalidation and naming things."
Cache invalidation removes or updates stale cached data. It's difficult because you're maintaining two copies (cache + DB) in sync.
Strategies
1. Time-Based (TTL)
Auto-expire after set duration. Simple but may serve stale data within window.
2. Event-Based
Invalidate when data changes via events/hooks. Always fresh but complex.
3. Version-Based
Key includes version: user:123:v5. Increment version on change. Old versions expire naturally.
Delete on Write (Best Practice)
- Update database
- Delete cache key (don't update — avoids race conditions)
- Next read refetches fresh data
Distributed Invalidation
With multiple services: publish data.changed event → all services invalidate their caches.
Key insight: TTL is your safety net. Even if event-based invalidation fails, TTL eventually clears stale data. Always combine both.
💻 Code Example
1// ============================================2// Cache Invalidation — Strategies3// ============================================45// ---------- TTL + Event Invalidation (Best Practice) ----------6class HybridCacheInvalidation {7 constructor(cache, db, eventBus) {8 this.cache = cache;9 this.db = db;10 this.eventBus = eventBus;11 this.defaultTTL = 3600;1213 this.eventBus.subscribe('data.changed', (event) => {14 this.cache.del(`\${event.type}:\${event.id}`);15 });16 }1718 async getProduct(productId) {19 const key = `product:\${productId}`;20 const cached = await this.cache.get(key);21 if (cached) return JSON.parse(cached);2223 const product = await this.db.findProduct(productId);24 if (product) await this.cache.set(key, JSON.stringify(product), 'EX', this.defaultTTL);25 return product;26 }2728 async updateProduct(productId, updates) {29 await this.db.updateProduct(productId, updates);30 await this.cache.del(`product:\${productId}`);31 await this.eventBus.publish('data.changed', { type: 'product', id: productId });32 }33}3435// ---------- Stampede Protection ----------36class StampedeProtectedCache {37 constructor(cache, db) { this.cache = cache; this.db = db; }3839 async getWithProtection(key, fetchFn, ttl = 3600) {40 const cached = await this.cache.get(key);41 if (cached) return JSON.parse(cached);4243 const lockKey = `lock:\${key}`;44 const acquired = await this.cache.set(lockKey, '1', 'PX', 5000, 'NX');4546 if (acquired === 'OK') {47 try {48 const rechecked = await this.cache.get(key);49 if (rechecked) return JSON.parse(rechecked);50 const data = await fetchFn();51 await this.cache.set(key, JSON.stringify(data), 'EX', ttl);52 return data;53 } finally {54 await this.cache.del(lockKey);55 }56 } else {57 await new Promise(r => setTimeout(r, 100));58 return this.getWithProtection(key, fetchFn, ttl);59 }60 }61}6263// ---------- Tag-Based Invalidation ----------64class TagBasedCache {65 constructor(cache) { this.cache = cache; }6667 async setWithTags(key, value, tags, ttl = 3600) {68 await this.cache.set(key, JSON.stringify(value), 'EX', ttl);69 for (const tag of tags) {70 await this.cache.sadd(`tag:\${tag}`, key);71 }72 }7374 async invalidateByTag(tag) {75 const keys = await this.cache.smembers(`tag:\${tag}`);76 if (keys.length > 0) {77 await this.cache.del(...keys);78 await this.cache.del(`tag:\${tag}`);79 }80 }81}8283console.log("Cache invalidation patterns demonstrated.");
🏋️ Practice Exercise
Race Condition Analysis: Two users simultaneously update the same product. Draw sequence diagrams for delete-on-write vs update-on-write invalidation.
TTL Strategy: Design TTLs for: (a) product prices (change 10x/day), (b) user sessions, (c) search results. Justify each.
Distributed Invalidation: 5 API servers with local + shared Redis cache. Design invalidation flow ensuring all caches are cleared.
Tag-Based Purge: Design tag-based invalidation for an e-commerce site where updating a category should invalidate all products in it.
⚠️ Common Mistakes
Updating cache instead of invalidating on write — race conditions can leave stale data. Always delete; let next read populate fresh data.
Not using TTL as safety net — event-based invalidation can fail. TTL ensures eventual freshness.
Forgetting distributed cache consistency — invalidating one server's cache doesn't invalidate others. Use pub/sub or shared cache.
Cache keys without namespacing — use 'service-name:user:123' to prevent cross-service conflicts.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Cache Invalidation Strategies