Skip to content
intermediatePhase 46 · Caching

Cache-Aside

Implement the most common caching pattern: check cache, then database.

45m
0 problems
Topic Progress0%

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

  1. Use consistent key naming: resource_type:id
  2. Set appropriate TTLs: Based on data freshness requirements
  3. Implement cache warming: For predictable access patterns
  4. Monitor hit ratios: Track effectiveness
  5. 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

0/3solved
Design Cache-Aside System

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 & reliability
Cache-Aside Scaling

How 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 decomposition
Cache-Aside Failure Modes

Analyze 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 degradation

Quiz

1. In Cache-Aside, who is responsible for loading data into the cache?

Question 1 options

2. What should you do to the cache when data is updated in the database?

Question 2 options

3. What is the main disadvantage of Cache-Aside on cache miss?

Question 3 options

4. Why is invalidation preferred over updating the cache on writes?

Question 4 options

5. Which key naming convention is recommended for Cache-Aside?

Question 5 options

Flashcards

Question

What is Cache-Aside pattern?

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?

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?

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

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?

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

  1. Read: Check Cache → [Hit] Return → [Miss] Query DB → Cache → Return
  2. 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)