Caching Patterns
There's no one-size-fits-all caching strategy. Different patterns suit different use cases. Let's master the three most important ones.
Pattern 1: Cache-Aside (Lazy Loading)
The most common pattern. The application manages both cache and database explicitly.
How It Works
- Application checks the cache first
- Cache hit - Return cached data
- Cache miss - Query the database
- Update cache with fresh data for next time
def get_user(user_id):
# Step 1: Check cache
cache_key = f"user:{user_id}"
cached_user = cache.get(cache_key)
if cached_user:
return cached_user # Cache hit!
# Step 2: Cache miss - query database
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
if user:
# Step 3: Populate cache for next time
cache.set(cache_key, user, ttl=3600) # 1 hour TTL
return user
Pros
- Only caches data that's actually requested
- Cache failures don't break the application
- Simple to implement and understand
Cons
- First request always hits the database
- Stale data possible if DB changes
- Cache stampede risk on popular keys
The Cache Stampede Problem
When a popular cache key expires, hundreds of requests might simultaneously hit the database:
Popular user's profile cache expires. 1000 concurrent requests all see "cache miss" and hammer the database simultaneously. Solution: Use cache locks or "stale-while-revalidate" pattern.
def get_user_with_lock(user_id):
cache_key = f"user:{user_id}"
lock_key = f"lock:user:{user_id}"
cached = cache.get(cache_key)
if cached:
return cached
# Try to acquire lock
if cache.set_nx(lock_key, "1", ttl=5): # NX = only if not exists
try:
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
cache.set(cache_key, user, ttl=3600)
return user
finally:
cache.delete(lock_key)
else:
# Another process is fetching - wait and retry
time.sleep(0.1)
return get_user_with_lock(user_id)
Pattern 2: Write-Through
Every write goes to both cache AND database synchronously.
How It Works
- Application writes to cache
- Cache writes to database (synchronously)
- Both succeed or both fail
def update_user(user_id, user_data):
cache_key = f"user:{user_id}"
# Write to database first
db.execute("UPDATE users SET ... WHERE id = ?", user_id)
# Then update cache
cache.set(cache_key, user_data, ttl=3600)
return user_data
Pros
- Cache always has latest data
- No stale data problems
- Reads are always fast
Cons
- Higher write latency (two writes)
- May cache data that's never read
- More complex failure handling
Use write-through when data consistency is critical and write volume is moderate. Common in session stores, user preferences, and shopping carts.
Pattern 3: Write-Behind (Write-Back)
Writes go to cache immediately; database is updated asynchronously.
How It Works
- Application writes to cache
- Cache acknowledges immediately (fast!)
- Cache asynchronously syncs to database
Write Request → Cache (instant) → Background Worker → Database
Pros
- Extremely fast writes
- Can batch database writes
- Absorbs traffic spikes
Cons
- Risk of data loss if cache fails
- Complex to implement correctly
- Eventual consistency only
If the cache crashes before syncing to the database, data is lost. Never use for financial transactions or critical data.
Pattern Comparison
| Pattern | Write Speed | Consistency | Data Loss Risk |
|---|---|---|---|
| Cache-Aside | N/A (read-focused) | Eventual | None |
| Write-Through | Slower | Strong | None |
| Write-Behind | Fastest | Eventual | High |
Read-Through Pattern
A variation where the cache itself fetches from the database on miss:
# The cache handles the DB lookup automatically
user = cache.get("user:123") # Returns from DB if not in cache
This simplifies application code but requires cache infrastructure support (like Redis modules or cache middleware).
Choosing the Right Pattern
Read-heavy workloads, unpredictable access patterns, when you want full control
Need consistency, moderate writes, session management, user preferences
High write throughput, analytics, logging, when data loss is acceptable
Key Takeaways
- Cache-aside is the most common - application controls everything
- Write-through keeps cache consistent but adds write latency
- Write-behind is fast but risky - use for non-critical data only
- Combine patterns - Cache-aside for reads, write-through for writes
Next up: Eviction Policies - What happens when your cache runs out of space?