🌐 Distributed Caching & Invalidation Patterns Masterclass
1️⃣ The Caching Paradigm: In-Process L1 vs Distributed L2
Definition: Distributed Caching ek aisi architectural pattern hai jisme high-cost storage tiers (SQL DBs, Microservices) se data fetch karne ke latency overhead ko minimize karne ke liye transient high-speed memory pools (RAM) use kiye jaate hain.
| Tier | Location & Examples | Latency | Consistency & Tradeoffs |
|---|---|---|---|
| L1: In-Process Cache | Application JVM / Process Memory (Caffeine, Go BigCache, Guava) | Sub-microsecond (< 100 ns) | Zero network hop; however, memory local hoti hai isliye horizontal replicas me consistency drift ho sakti hai. |
| L2: Distributed Cache | Dedicated Cluster (Redis Cluster, Memcached) | Sub-millisecond (0.5 - 2 ms) | Shared single source of truth for all backend nodes; network roundtrip require karta hai. |
2️⃣ Core Read/Write Topologies
| Pattern | Read Workflow | Write Workflow | Primary Advantage |
|---|---|---|---|
| Cache-Aside (Lazy Loading) | App reads Cache → on miss reads DB → writes to Cache. | App writes directly to DB → Evicts / Invalidates Cache key. | Only requested keys occupy RAM; resilient to cache crashes. |
| Write-Through | App reads through Cache layer. | App writes to Cache → Cache plugin synchronously writes to DB. | Data is never stale in cache; higher write latency. |
| Write-Behind (Write-Back) | App reads through Cache layer. | App writes to Cache → async worker batches writes to DB. | Ultra-high write throughput; risk of data loss if cache dies before DB flush. |
// Production Cache-Aside Read Pattern with Fallback
async function getJobDetails(jobId) {
const cacheKey = `job:${jobId}`;
// 1. Check L2 Redis Cache
const cachedData = await redis.get(cacheKey);
if (cachedData) {
return JSON.parse(cachedData); // Cache Hit
}
// 2. Cache Miss: Query Database
const dbData = await db.jobs.findById(jobId);
if (!dbData) {
// Prevent Cache Penetration: Cache null with short 60s TTL
await redis.setex(cacheKey, 60, JSON.stringify(null));
return null;
}
// 3. Populate Cache with jittered TTL (1 hour +- 5 mins)
const jitter = Math.floor(Math.random() * 600) - 300;
await redis.setex(cacheKey, 3600 + jitter, JSON.stringify(dbData));
return dbData;
}
3️⃣ The Cache Invalidation Dilemma: Update vs Evict
"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton.
Agar do concurrent write requests (Thread A aur Thread B) aati hain:
1. Thread A updates DB with value 1.
2. Thread B updates DB with value 2.
3. Agar network latency ke karan Thread B pehle cache update kar de aur Thread A baad me, toh Cache me Value 1 (stale) hamesha ke liye persist ho jayegi jabki DB me Value 2 hai!
Remedy: Update query ke baad key ko DELETE karein taaki agli read request fresh DB data se populate ho sake.
4️⃣ Cache Stampede (Thundering Herd) & Locking
Jab koi high-traffic hot key expire hoti hai, toh hazaron concurrent requests ek sath database par hammer karti hain, jisse database crash ho jata hai:
| Mitigation Technique | Mechanism | Tradeoff |
|---|---|---|
| Distributed Mutex Lock | Sirf pehla request lock (SET resource_key token NX EX 5) lekar DB query karta hai, baaki requests sleep karke retry karti hain. |
Waiting requests me temporary latency increase hoti hai. |
| Probabilistic Early Expiration (XFetch) | Key expire hone se pehle probabilistic formula ke basis par single background worker cache refresh kar deta hai: $$-\beta \times \Delta \times \ln(\text{random}()) > \text{TTL}_{\text{remaining}}$$ | Zero latency spike; requires compute overhead on reads. |
5️⃣ Penetration, Avalanche & Breakdown Defenses
| Failure Pattern | Root Cause | Production Defense |
|---|---|---|
| Cache Penetration | Attacker repeatedly queries non-existent IDs (e.g. id=-9999) that bypass cache and hit DB directly. |
Bloom Filter (checks existence before querying) OR cache empty null values with short TTL. |
| Cache Avalanche | Thousands of keys same time par expire hone se DB par mass traffic shift ho jana. | TTL Jitter: Har key ke expiration time me random duration add karna (e.g. $3600 \pm 300\text{ seconds}$). |
| Cache Breakdown | Single ultra-popular hot key expire hona aur massive spikes generate karna. | Distributed Mutex Locking / Perpetual logical keys with async refresh. |
6️⃣ Two-Tier Hybrid Caching Architecture (L1 + L2)
Enterprise applications ultra-low latency ke liye Local Memory (L1) aur Redis Cluster (L2) ko combine karte hain:
# Two-Tier Read Pipeline:
Incoming Request → Check L1 In-Memory Cache (0.1ms)
├── If Hit → Return immediately (Zero network cost)
└── If Miss → Check L2 Distributed Redis Cache (1.2ms)
├── If Hit → Populate L1 Cache → Return
└── If Miss → Fetch from DB (15ms) → Populate L2 & L1 → Return
# Invalidation Synchronization over Redis Pub/Sub:
Node A updates DB → Evicts L2 Redis → Publishes "invalidate:job:401"
└── Node B & Node C listen via Pub/Sub → Instantly purge local L1 memory!
7️⃣ Change Data Capture (CDC) & Outbox Pattern
Direct application code se cache evict karne par network failure ya application crash se DB aur Cache desynchronize ho sakte hain. CDC (Debezium + Kafka) database transaction logs (Write-Ahead Log / WAL) ko stream karke zero-data-loss asynchronous cache invalidation provide karta hai:
App → Commit to Postgres → Postgres WAL Log → Debezium CDC Engine → Kafka Topic → Invalidation Worker → Evict Redis KeyIs approach se application code ko caching logic ki chinta nahi karni padti aur multi-region caches guaranteed consistency achieve karte hain.