SLIDE 78: Section 4: Caching, Queues, and Async Workflows
The Building Blocks of Scalable Systems
Why This Section Matters:
Key Insight:
Interview designs show a cache box. Production designs specify TTL, invalidation strategy, failure modes, and cache warming.
SLIDE 79: Caching: Why It Matters
The Performance Impact of Caching
Without Cache:
With Cache:
Cache Hit Rate Math:
If 95% cache hit rate:
- 95% of requests: 1ms (cache)
- 5% of requests: 50ms (database)
Average latency = (0.95 x 1) + (0.05 x 50) = 3.45ms
SLIDE 80: Caching: Cache Layers
The Caching Hierarchy
Cache Layers Explained:
Request Flow:
SLIDE 81: Caching: Cache-Aside Pattern
The Most Common Caching Pattern
How It Works:
Implementation:
def get_user(user_id):
# Step 1: Check cache
cache_key = f"user:{user_id}"
cached = cache.get(cache_key)
if cached:
return cached # Cache hit
# Step 2: Cache miss - query database
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
# Step 3: Store in cache for next time
cache.set(cache_key, user, ttl=3600) # 1 hour TTL
return user
Characteristics:
SLIDE 82: Caching: Read-Through and Write-Through
Alternative Caching Patterns
Read-Through Cache:
# Application only talks to cache
# Cache handles database interaction
def get_user(user_id):
return cache.get(f"user:{user_id}") # Cache fetches from DB if miss
Write-Through Cache:
def update_user(user_id, data):
# Write to cache, cache writes to database
cache.set(f"user:{user_id}", data) # Cache persists to DB
SLIDE 83: Caching: Read-Through and Write-Through (continued)
Write-Behind (Write-Back) Cache:
def update_user(user_id, data):
# Write to cache immediately, return to user
cache.set(f"user:{user_id}", data)
# Cache persists to DB asynchronously (batched)
Pattern Comparison:
Production Recommendation:
Cache-aside is most common. Use write-through for critical data. Avoid write-behind unless you can tolerate data loss.
SLIDE 84: Caching: TTL Strategies
Time-To-Live: When Cache Entries Expire
Why TTL Matters:
Without TTL:
- Cache grows unbounded
- Stale data never refreshed
- Memory exhaustion
With TTL:
- Automatic expiration
- Bounded staleness
- Memory recycled
TTL Guidelines:
SLIDE 85: Caching: TTL Strategies (continued)
TTL Trade-offs:
Dynamic TTL:
def get_ttl(data_type, popularity):
base_ttl = {
"user_profile": 3600,
"feed": 300,
"trending": 60
}
# Popular items get longer TTL (more cache hits)
if popularity > 1000:
return base_ttl[data_type] * 2
return base_ttl[data_type]
Production Practice:
Start with longer TTL, reduce if consistency problems arise. Monitor cache hit rate and staleness complaints.
SLIDE 86: Caching: Cache Invalidation
The Two Hard Problems in Computer Science
"There are only two hard things in Computer Science:
cache invalidation and naming things."
- Phil Karlton
Why Invalidation is Hard:
Invalidation Strategies:
SLIDE 87: Caching: Cache Invalidation (continued)
Strategy 1: TTL Expiration (Passive)
def update_user(user_id, data):
db.update("UPDATE users SET ... WHERE id = ?", data, user_id)
# Do nothing to cache
# Cache will expire based on TTL
Stale for up to TTL duration. Simple but inconsistent.
Strategy 2: Explicit Delete (Active)
def update_user(user_id, data):
db.update("UPDATE users SET ... WHERE id = ?", data, user_id)
cache.delete(f"user:{user_id}") # Invalidate cache
Next read will repopulate cache with fresh data.
Strategy 3: Explicit Update (Active)
def update_user(user_id, data):
db.update("UPDATE users SET ... WHERE id = ?", data, user_id)
cache.set(f"user:{user_id}", data) # Update cache
Cache always has latest data. Risk: database write succeeds but cache update fails.
SLIDE 88: Caching: Cache Invalidation (continued)
Strategy 4: Publish-Subscribe Invalidation
# On write
def update_user(user_id, data):
db.update("UPDATE users SET ... WHERE id = ?", data, user_id)
event_bus.publish("user.updated", {"user_id": user_id})
# Subscriber
def on_user_updated(event):
cache.delete(f"user:{event.user_id}")
When to Use Each:
SLIDE 89: Caching: Thundering Herd Problem
When Cache Expiration Causes Outage
The Scenario:
1. Popular item cached with TTL = 60 seconds
2. 10,000 users viewing this item
3. Cache expires at t=60
4. All 10,000 users request simultaneously
5. All 10,000 requests hit database
6. Database overwhelmed, crashes
Why It Happens:
SLIDE 90: Caching: Thundering Herd Solutions
Solution 1: Jittered TTL
def cache_with_jitter(key, value, base_ttl):
# Add random jitter: TTL = base + random(0, 10%)
jitter = random.uniform(0, base_ttl * 0.1)
ttl = base_ttl + jitter
cache.set(key, value, ttl=ttl)
Cache entries expire at slightly different times, spreading the load.
Solution 2: Lock and Single Fetch
def get_with_lock(key, fetch_func):
value = cache.get(key)
if value:
return value
# Try to acquire lock
lock_key = f"lock:{key}"
if cache.set(lock_key, "1", nx=True, ex=5): # Lock for 5 seconds
# I have the lock, fetch data
value = fetch_func()
cache.set(key, value, ttl=3600)
cache.delete(lock_key)
return value
else:
# Someone else is fetching, wait and retry
time.sleep(0.1)
return get_with_lock(key, fetch_func)
Only one request fetches from database, others wait.
SLIDE 91: Caching: Thundering Herd Solutions (continued)
Solution 3: Stale-While-Revalidate
def get_with_stale(key, fetch_func, ttl=3600, stale_ttl=300):
cached = cache.get(key)
if cached:
if cached.age < ttl:
return cached.value # Fresh
elif cached.age < ttl + stale_ttl:
# Stale but acceptable
# Trigger async refresh
queue.enqueue("refresh_cache", key)
return cached.value # Return stale
# No cache or too stale
value = fetch_func()
cache.set(key, value, ttl=ttl + stale_ttl)
return value
Return stale data immediately while refreshing in background.
Solution Comparison:
SLIDE 92: Caching: Hot Key Problem
When One Key Gets All Traffic
The Problem:
Key: "trending_post:123"
Traffic: 100,000 requests/second
Single Redis server: 50,000 ops/second max
Result: Redis overloaded, latency spikes
Why It Happens:
SLIDE 93: Caching: Hot Key Solutions
Solution 1: Local Caching
# In-memory cache on application server
local_cache = {}
local_cache_ttl = {}
def get_with_local_cache(key):
# Check local cache first (no network)
if key in local_cache and local_cache_ttl[key] > time.now():
return local_cache[key]
# Check Redis
value = redis.get(key)
if value and is_hot_key(key):
# Cache hot keys locally for 1 second
local_cache[key] = value
local_cache_ttl[key] = time.now() + 1
return value
Hot keys cached in application memory. Reduces Redis load.
SLIDE 94: Caching: Hot Key Solutions (continued)
Solution 2: Key Replication
def set_hot_key(key, value):
# Replicate to multiple keys
for i in range(10):
redis.set(f"{key}:{i}", value)
def get_hot_key(key):
# Randomly pick one replica
replica = random.randint(0, 9)
return redis.get(f"{key}:{replica}")
Distributes load across multiple keys (and potentially multiple Redis nodes).
Solution 3: Read Replicas
Route hot key reads to dedicated read replicas.
SLIDE 95: Caching: Cache Warming
Pre-Populating Cache Before Traffic
When to Warm Cache:
Cache Warming Implementation:
def warm_cache():
# Warm most popular users
popular_users = db.query("""
SELECT id FROM users
ORDER BY follower_count DESC
LIMIT 10000
""")
for user in popular_users:
user_data = db.query("SELECT * FROM users WHERE id = ?", user.id)
cache.set(f"user:{user.id}", user_data, ttl=3600)
print(f"Warmed cache with{len(popular_users)} users")
SLIDE 96: Caching: Cache Warming (continued)
Warming Strategies:
Gradual Warming:
def gradual_warm(items, batch_size=100, delay=0.1):
for i in range(0, len(items), batch_size):
batch = items[i:i + batch_size]
for item in batch:
warm_item(item)
# Pause to avoid overwhelming database
time.sleep(delay)
Production Practice:
- Warm cache during deployment before accepting traffic
- Monitor cache hit rate during warming
- Have fallback if warming fails (serve from DB)
SLIDE 97: CDN: Content Delivery Networks
Serving Static Content Globally
Without CDN:
Round trip: 400ms minimum due to speed of light.
With CDN:
Round trip: 40ms. Content served from nearby edge server.
CDN Benefits:
SLIDE 98: CDN: Static vs Dynamic Content
What to Cache on CDN
Static Content (Always Cache):
Dynamic Content (Selective Cache):
Never Cache:
- Authenticated user data
- Shopping cart
- Payment information
- Real-time data
SLIDE 99: CDN: Cache Headers
Controlling CDN Behavior
Key Headers:
Cache-Control Directives:
# Cache for 1 year (immutable assets)
Cache-Control: public, max-age=31536000, immutable
# Cache for 5 minutes, revalidate after
Cache-Control: public, max-age=300, stale-while-revalidate=60
# Do not cache at all
Cache-Control: no-store, no-cache, must-revalidate
# Cache privately (browser only, not CDN)
Cache-Control: private, max-age=3600
SLIDE 100: CDN: Cache Headers (continued)
Versioned URLs for Cache Busting:
Implementation:
When file changes, hash changes, URL changes, CDN fetches new version.
SLIDE 101: CDN: Invalidation
Clearing CDN Cache
Invalidation Methods:
When to Purge:
- Security vulnerability in JavaScript
- Incorrect data served
- Legal/compliance issue
- Breaking bug in production
Do NOT purge for:
- Normal content updates (use versioned URLs)
- Minor fixes (wait for TTL)
Purge Example (Cloudflare):
import cloudflare
def purge_url(url):
cf = cloudflare.Client(api_key="...")
cf.zones.purge_cache.post(
zone_id="...",
data={"files": [url]}
)