How Cache-Aside Works
How Cache-Aside Works
Cache-Aside (also called Lazy Loading) is the most common caching pattern. The application code is responsible for managing the cache.
Flow Diagram
Cache-Aside Flow:
1. READ (Cache Hit):
App → Check Cache → [HIT] → Return Data
↓
[MISS] → Query DB → Store in Cache → Return Data
2. WRITE:
App → Write to DB → Invalidate Cache (delete)
Read Path
def get_user(user_id):
# Step 1: Check cache first
cache_key = f"user:{user_id}"
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data # Cache Hit
# Step 2: Cache miss - query database
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
# Step 3: Store in cache for future requests
cache.set(cache_key, user, ttl=3600)
return user
Write Path
def update_user(user_id, data):
# Step 1: Write to database first
db.query("UPDATE users SET ... WHERE id = ?", user_id)
# Step 2: Invalidate cache (not update)
cache.delete(f"user:{user_id}")
# Next read will populate cache with fresh data
Why Invalidate, Not Update?
- Simpler logic
- Avoids race conditions
- Data consistency guaranteed on next read
- Less cache write operations
Implementation
Cache-Aside Implementation
Complete Implementation Example
class CacheAsideManager:
def __init__(self, cache_client, db_client):
self.cache = cache_client
self.db = db_client
self.default_ttl = 3600 # 1 hour
def get(self, key, loader_func, ttl=None):
"""
Generic cache-aside get method
"""
# Try cache first
value = self.cache.get(key)
if value is not None:
return value
# Cache miss - load from source
value = loader_func()
# Store in cache
if value is not None:
self.cache.set(key, value, ttl or self.default_ttl)
return value
def invalidate(self, key):
"""Invalidate cache entry"""
self.cache.delete(key)
def invalidate_pattern(self, pattern):
"""Invalidate all keys matching pattern"""
keys = self.cache.keys(pattern)
if keys:
self.cache.delete(*keys)
# Usage
manager = CacheAsideManager(redis_client, db_client)
# Get user with cache-aside
def get_user(user_id):
return manager.get(
key=f"user:{user_id}",
loader_func=lambda: db.query("SELECT * FROM users WHERE id = ?", user_id),
ttl=1800
)
# Update user with cache invalidation
def update_user(user_id, data):
db.query("UPDATE users SET ... WHERE id = ?", user_id)
manager.invalidate(f"user:{user_id}")
Handling Race Conditions
def get_with_lock(key, loader_func, ttl=3600):
"""Cache-Aside with distributed lock for cache stampede prevention"""
# Try cache
value = cache.get(key)
if value is not None:
return value
# Acquire lock
lock_key = f"lock:{key}"
if lock.acquire(lock_key, timeout=5):
try:
# Double-check after acquiring lock
value = cache.get(key)
if value is not None:
return value
# Load and cache
value = loader_func()
cache.set(key, value, ttl)
return value
finally:
lock.release(lock_key)
else:
# Another process is loading, wait and retry
time.sleep(0.1)
return cache.get(key)
Best Practices
- Use consistent key naming:
resource_type:id - Set appropriate TTLs: Based on data freshness requirements
- Implement cache warming: For predictable access patterns
- Monitor hit ratios: Track effectiveness
- Handle null values: Cache negative results briefly
Pros and Cons
Pros and Cons of Cache-Aside
Advantages
| Advantage | Description |
|---|---|
| Simple to implement | Application controls all cache logic |
| Lazy loading | Only caches data that's actually requested |
| No wasted resources | Empty cache doesn't cause issues |
| Flexible TTLs | Different data can have different expiration |
| Easy invalidation | Direct control over when to invalidate |
| Battle-tested | Most widely used pattern |
Disadvantages
| Disadvantage | Description |
|---|---|
| Cache miss penalty | First request always hits database |
| Data inconsistency | Brief window between DB write and cache invalidation |
| Cache warming needed | Cold start causes performance issues |
| Application complexity | Cache logic mixed with business logic |
| Stale data risk | TTL might serve outdated data |
Comparison with Other Patterns
Pattern Comparison:
Cache-Aside:
- App manages cache
- Lazy loading
- Simple but more code
Read-Through:
- Cache manages loading
- Automatic population
- Less app code
Write-Through:
- Synchronous writes
- Strong consistency
- Higher write latency
Write-Behind:
- Asynchronous writes
- Better performance
- Risk of data loss
When to Use Cache-Aside
Good for:
- General-purpose caching
- Read-heavy workloads
- When you need fine-grained control
- Applications with simple data models
Not ideal for:
- Write-heavy workloads (consider Write-Through)
- When consistency is critical (consider Read-Through)
- Complex data relationships (consider application-level caching)
Practice Problems
Design a scalable Cache-Aside system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Cache-Aside to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Cache-Aside and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. In Cache-Aside, who is responsible for loading data into the cache?
2. What should you do to the cache when data is updated in the database?
3. What is the main disadvantage of Cache-Aside on cache miss?
4. Why is invalidation preferred over updating the cache on writes?
5. Which key naming convention is recommended for Cache-Aside?
Flashcards
Question
What is Cache-Aside pattern?
Click to reveal answer
Answer
A caching pattern where the application code manages the cache: checks cache first, loads from DB on miss, and invalidates cache on write
Question
Should you update or invalidate cache on write in Cache-Aside?
Click to reveal answer
Answer
Invalidate (delete) the cache entry. This avoids race conditions and ensures the next read gets fresh data.
Question
What is the cache miss penalty in Cache-Aside?
Click to reveal answer
Answer
The first request for any data must go to the database before it can be cached, causing higher latency for that request.
Question
Name 3 advantages of Cache-Aside
Click to reveal answer
Answer
1) Simple to implement, 2) Lazy loading (no wasted resources), 3) Flexible TTLs per data type
Question
What is a common Cache-Aside key format?
Click to reveal answer
Answer
resource_type:id (e.g., 'user:123', 'product:456') for readability and pattern-based invalidation
Revision Notes
Key Takeaways
- 1.Cache-Aside is the most common caching pattern where the application manages the cache
- 2.Always invalidate (delete) cache on write, don't update - avoids race conditions
- 3.Cache miss penalty means first request always hits the database
- 4.Use distributed locks to prevent cache stampede on popular keys
- 5.Consistent key naming (resource_type:id) simplifies management
Interview Tips
- •Draw the flow diagram: App → Cache → DB showing both hit and miss paths
- •Explain why invalidation is preferred over update on writes
- •Discuss cache stampede prevention with locking patterns
- •Mention monitoring cache hit ratio as a key operational concern
Cheat Sheet
Cheat Sheet: Cache-Aside
Flow
- Read: Check Cache → [Hit] Return → [Miss] Query DB → Cache → Return
- Write: Write DB → Invalidate Cache
Implementation
value = cache.get(key)
if value is None:
value = db.query(...)
cache.set(key, value, ttl)
return value
Key Points
- Application manages all cache logic
- Use invalidation, not update, on writes
- Handle race conditions with locks
- Monitor cache hit ratio
- Use consistent key naming (resource:id)