Cache Invalidation Strategies

0/4 in this phase0/45 across the roadmap

📖 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)

  1. Update database
  2. Delete cache key (don't update — avoids race conditions)
  3. 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

codeTap to expand ⛶
1// ============================================
2// Cache Invalidation — Strategies
3// ============================================
4
5// ---------- 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;
12
13 this.eventBus.subscribe('data.changed', (event) => {
14 this.cache.del(`\${event.type}:\${event.id}`);
15 });
16 }
17
18 async getProduct(productId) {
19 const key = `product:\${productId}`;
20 const cached = await this.cache.get(key);
21 if (cached) return JSON.parse(cached);
22
23 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 }
27
28 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}
34
35// ---------- Stampede Protection ----------
36class StampedeProtectedCache {
37 constructor(cache, db) { this.cache = cache; this.db = db; }
38
39 async getWithProtection(key, fetchFn, ttl = 3600) {
40 const cached = await this.cache.get(key);
41 if (cached) return JSON.parse(cached);
42
43 const lockKey = `lock:\${key}`;
44 const acquired = await this.cache.set(lockKey, '1', 'PX', 5000, 'NX');
45
46 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}
62
63// ---------- Tag-Based Invalidation ----------
64class TagBasedCache {
65 constructor(cache) { this.cache = cache; }
66
67 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 }
73
74 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}
82
83console.log("Cache invalidation patterns demonstrated.");

🏋️ Practice Exercise

  1. Race Condition Analysis: Two users simultaneously update the same product. Draw sequence diagrams for delete-on-write vs update-on-write invalidation.

  2. TTL Strategy: Design TTLs for: (a) product prices (change 10x/day), (b) user sessions, (c) search results. Justify each.

  3. Distributed Invalidation: 5 API servers with local + shared Redis cache. Design invalidation flow ensuring all caches are cleared.

  4. 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