Caching (Component Deep Dive)¶
What is Caching?¶
Caching stores frequently accessed data in a fast storage layer to reduce latency and load on backend systems.
Benefits: - ⚡ Faster response times (RAM access ~1ms vs DB query ~100ms) - 💰 Cost reduction (fewer DB reads, less compute) - 📈 Scalability (handle more traffic with same backend) - 🛡️ Reliability (serve cached data if backend fails)
Cache Hierarchy¶
L1: Browser Cache (1-5ms)
↓
L2: CDN Cache (10-50ms)
↓
L3: Application Cache / Redis (1-10ms)
↓
L4: Database Query Cache (10-50ms)
↓
Database (100-500ms)
Caching Strategies¶
1. Cache-Aside (Lazy Loading) ⭐ Most Common¶
FUNCTION getUser(userId)
# 1. Check cache first
cached = CACHE_GET("user:" + userId)
IF cached EXISTS THEN
RETURN deserialize(cached)
END IF
# 2. Cache miss - fetch from database
user = DB_QUERY("SELECT * FROM users WHERE id = ?", userId)
# 3. Update cache for next time (TTL = 1 hour)
CACHE_SET("user:" + userId, serialize(user), TTL = 3600)
RETURN user
END FUNCTION
Pros: - ✅ Only requested data is cached (efficient memory usage) - ✅ Resilient to cache failures (app still works) - ✅ Simple to implement
Cons: - ❌ Cache miss penalty (first request is slow) - ❌ Stale data possible if not invalidated properly - ❌ Each cache miss = 3 round trips (check cache, query DB, update cache)
Use Cases: - Read-heavy workloads - User profiles, product catalogs - Data that doesn't change frequently
2. Write-Through Cache¶
FUNCTION updateUser(userId, data)
# 1. Write to database
DB_QUERY("UPDATE users SET name = ? WHERE id = ?", data.name, userId)
# 2. Immediately update cache
CACHE_SET("user:" + userId, serialize(data), TTL = 3600)
RETURN data
END FUNCTION
Pros: - ✅ Cache always consistent with database - ✅ No cache miss for recently written data - ✅ Data durability (written to DB immediately)
Cons: - ❌ Slower writes (wait for both DB + cache) - ❌ Cache may store data that's never read (wasted memory) - ❌ Write latency increases
Use Cases: - Applications requiring strong consistency - Read-after-write patterns - Banking, financial transactions
3. Write-Behind (Write-Back) Cache¶
FUNCTION updateUser(userId, data)
# 1. Write to cache immediately (fast)
CACHE_SET("user:" + userId, serialize(data), TTL = 3600)
# 2. Asynchronously write to database (background job)
QUEUE_ENQUEUE("writeUserToDB", { userId, data })
RETURN data
END FUNCTION
# Background worker
FUNCTION writeUserToDB(job)
userId = job.userId
data = job.data
DB_QUERY("UPDATE users SET name = ? WHERE id = ?", data.name, userId)
END FUNCTION
Pros: - ✅ Very fast writes (only cache write is synchronous) - ✅ Batch writes to DB (better throughput) - ✅ Reduces database load significantly
Cons: - ❌ Risk of data loss if cache crashes before DB write - ❌ Complex implementation (need reliable queue) - ❌ Eventual consistency
Use Cases: - High write throughput systems - Gaming leaderboards, analytics dashboards - When eventual consistency is acceptable
4. Refresh-Ahead (Proactive)¶
# Set cache with TTL
CACHE_SET("user:" + userId, serialize(user), TTL = 3600)
# Before expiration, refresh cache (runs periodically every minute)
EVERY 60 seconds DO
ttl = CACHE_GET_TTL("user:" + userId)
IF ttl < 300 THEN # Refresh if < 5 minutes remaining
freshUser = DB_QUERY("SELECT * FROM users WHERE id = ?", userId)
CACHE_SET("user:" + userId, serialize(freshUser), TTL = 3600)
END IF
END EVERY
Pros: - ✅ Minimizes cache miss penalty - ✅ Predictable response times - ✅ Good for frequently accessed data
Cons: - ❌ Difficult to predict which data to refresh - ❌ Wasted refreshes if data not accessed - ❌ Increased complexity
Use Cases: - Homepage content, trending products - Data accessed predictably (daily reports) - Real-time dashboards
Cache Eviction Policies¶
1. LRU (Least Recently Used) ⭐ Most Common¶
Evict the entry that was accessed longest ago. Implemented with a hash map + doubly linked list (or an ordered map) for O(1) get/put - see the worked implementation in LRU Cache.
Pros: Evicts truly unused data
Cons: Doesn't account for access frequency
Use: General-purpose caching
2. LFU (Least Frequently Used)¶
Evict the entry with the lowest access count (track a frequency counter per key; O(1) with a frequency-bucket structure).
Pros: Keeps "hot" data in cache
Cons: Old popular items are hard to evict (frequency doesn't decay)
Use: Recommendation systems, analytics
3. FIFO (First In First Out)¶
Pros: Simple to implement
Cons: May evict frequently used data
Use: Simple caching needs
4. TTL (Time To Live)¶
CACHE_SET("session:abc123", sessionData, TTL = 900) # 15 minutes
# Auto-expires after TTL (the cache removes the key when TTL elapses)
AFTER 900 seconds DO
CACHE_DELETE("session:abc123")
END AFTER
Pros: Automatic expiration, prevents stale data
Cons: Arbitrary expiration time
Use: Session storage, temporary data
TTL Best Practices¶
# Different TTLs for different data types
TTLs = {
userProfile: 3600, # 1 hour
productCatalog: 600, # 10 minutes
homePageData: 60, # 1 minute
sessionData: 1800, # 30 minutes
apiResponse: 300 # 5 minutes
}
# Dynamic TTL based on data freshness
FUNCTION calculateTTL(data)
IF data.changesOften THEN RETURN 60 # 1 minute
IF data.changesDaily THEN RETURN 86400 # 24 hours
RETURN 3600 # Default 1 hour
END FUNCTION
Cache Invalidation¶
"There are only two hard things in Computer Science: cache invalidation and naming things." - Phil Karlton
1. TTL-Based (Passive)¶
2. Event-Based (Active)¶
# On data update
FUNCTION updateUser(userId, data)
DB_UPDATE("users", userId, data)
# Invalidate cache
CACHE_DELETE("user:" + userId)
# Or update cache immediately (write-through)
CACHE_SET("user:" + userId, serialize(data), TTL = 3600)
END FUNCTION
3. Tag-Based Invalidation¶
# Tag cache entries
CACHE_SET("user:123", userData)
SET_ADD("tags:user:123", "user:123")
SET_ADD("tags:team:5", "user:123") # User belongs to team 5
# Invalidate all cache entries for team 5
FUNCTION invalidateTeam(teamId)
keys = SET_MEMBERS("tags:team:" + teamId)
CACHE_DELETE(keys) # delete all matching keys
END FUNCTION
Cache Stampede (Thundering Herd)¶
Problem:¶
Solution 1: Lock-Based¶
locked = new Set()
FUNCTION getUser(userId)
cached = CACHE_GET("user:" + userId)
IF cached EXISTS THEN RETURN deserialize(cached)
# Only first request fetches from DB
IF locked.contains(userId) THEN
SLEEP(100 ms) # Wait for first request to populate cache
RETURN getUser(userId) # Retry
END IF
locked.add(userId)
TRY
user = DB_QUERY("SELECT * FROM users WHERE id = ?", userId)
CACHE_SET("user:" + userId, serialize(user), TTL = 3600)
RETURN user
FINALLY
locked.remove(userId)
END TRY
END FUNCTION
Solution 2: Probabilistic Early Expiration¶
FUNCTION getUser(userId)
cached = CACHE_GET("user:" + userId)
ttl = CACHE_GET_TTL("user:" + userId)
# Randomly refresh before expiration
shouldRefresh = (ttl < 300) AND (RANDOM() < 0.1)
IF cached EXISTS AND NOT shouldRefresh THEN
RETURN deserialize(cached)
END IF
# Refresh cache
user = DB_QUERY("SELECT * FROM users WHERE id = ?", userId)
CACHE_SET("user:" + userId, serialize(user), TTL = 3600)
RETURN user
END FUNCTION
Hot Key Problem¶
Problem:¶
One key (e.g., celebrity user profile) gets 1M req/sec → Single cache server overloaded
Solutions:¶
1. Local Caching¶
# Cache hot data in application memory
local_cache = empty map
FUNCTION getUser(userId)
# Check local cache first (fastest)
IF localCache.contains(userId) THEN
RETURN localCache.get(userId)
END IF
# Check distributed cache
cached = CACHE_GET("user:" + userId)
IF cached EXISTS THEN
user = deserialize(cached)
localCache.put(userId, user) # Store locally
RETURN user
END IF
# Fetch from DB
user = DB_QUERY("SELECT * FROM users WHERE id = ?", userId)
CACHE_SET("user:" + userId, serialize(user), TTL = 3600)
localCache.put(userId, user)
RETURN user
END FUNCTION
2. Replication¶
Hot key replicated across multiple cache servers:
user:celebrity → user:celebrity:1, user:celebrity:2, user:celebrity:3
Randomly select replica:
replica = random integer in [1, 3]
key = "user:celebrity:" + replica
Cache Efficiency Metrics¶
Track hits and misses; hit rate = hits / (hits + misses). Target >80%.
Key Metrics: - Hit Rate: Hits / (Hits + Misses) - Target: >80% - Miss Rate: Misses / (Hits + Misses) - Lower is better - Eviction Rate: How often cache is full and evicts data - Memory Usage: Current usage vs capacity - Latency: P50, P95, P99 response times
Distributed Caching¶
Single Server Cache (Simple)¶
Cache Cluster (Scalable) ⭐¶
App Servers ──→ Hash(key) % num_servers
↓
Server 1 (keys A-H)
Server 2 (keys I-P)
Server 3 (keys Q-Z)
Consistent Hashing ⭐ (Handles server addition/removal)¶
Map both nodes and keys onto a hash ring; a key belongs to the first node clockwise. Adding or
removing a node only remaps ~1/N of keys (vs hash(key) % N, which remaps almost everything).
Use virtual nodes (e.g. 150 per physical node) for even distribution. Lookup is O(log N) via
binary search over the sorted ring. See the worked pseudocode in
Distributed Cache.
CDN (Content Delivery Network)¶
User in US → US CDN Edge → Origin Server (California)
User in EU → EU CDN Edge → Origin Server (California)
User in Asia → Asia CDN Edge → Origin Server (California)
Benefits:
✅ Reduced latency (serve from nearest edge)
✅ Reduced bandwidth costs (offload origin)
✅ Better availability (distributed)
✅ DDoS protection
CDN Best Practices:
# Set cache headers for static assets
SET_RESPONSE_HEADER("Cache-Control", "public, max-age=31536000, immutable")
# Versioned URLs for cache busting
# reference the asset as: /js/app.v123.js
# When app.js changes, deploy as app.v124.js
# Browser fetches new version, old version cached forever
Common Pitfalls¶
| Problem | Solution |
|---|---|
| Stale data | Use TTL + event-based invalidation |
| Cache stampede | Use locks or probabilistic expiration |
| Memory overflow | Set max memory + eviction policy |
| Hot keys | Use local cache or replication |
| Cache inconsistency | Use write-through or invalidation |
| Over-caching | Cache only frequently accessed data |
Multi-Level Caching Architecture¶
Request Flow:
1. Browser Cache (200ms saved)
↓ miss
2. CDN (500ms saved)
↓ miss
3. Application In-Memory Cache (800ms saved)
↓ miss
4. Redis Cluster (900ms saved)
↓ miss
5. Database Query Cache (950ms saved)
↓ miss
6. Database (1000ms)
Example Implementation:
FUNCTION getProduct(productId)
# L1: In-memory cache (fastest, small)
IF memoryCache.contains(productId) THEN
RETURN memoryCache.get(productId)
END IF
# L2: Distributed cache (fast, larger)
cached = CACHE_GET("product:" + productId)
IF cached EXISTS THEN
product = deserialize(cached)
memoryCache.put(productId, product) # Populate L1
RETURN product
END IF
# L3: Database (slow, largest)
product = DB_QUERY("SELECT * FROM products WHERE id = ?", productId)
# Populate all cache levels
CACHE_SET("product:" + productId, serialize(product), TTL = 3600)
memoryCache.put(productId, product)
RETURN product
END FUNCTION
When to Cache?¶
Cache if: - ✅ Data is read frequently (>10x reads vs writes) - ✅ Data is expensive to compute/fetch - ✅ Data doesn't change often - ✅ Slight staleness is acceptable
Don't cache if: - ❌ Data changes very frequently - ❌ Strong consistency required - ❌ Data is accessed rarely - ❌ Data is already fast to fetch
Summary¶
Caching is essential for: - ⚡ Performance (reduce latency from 100ms → 1ms) - 💰 Cost savings (reduce database load by 90%+) - 📈 Scalability (handle 10x more traffic)
Best Practices: - Use cache-aside for read-heavy workloads - Implement TTL-based + event-based invalidation - Monitor cache hit rate (target >80%) - Use LRU eviction policy for general use - Prevent cache stampede with locks - Use consistent hashing for distributed cache - Implement multi-level caching for high traffic
Next Steps: - Learn about Load Balancer for distribution - Understand Database Sharding for scaling data