# Trade-offs Between Different Caching Strategies: 8 Patterns Explained

> Explore 8 caching strategies and their trade-offs. Understand how to balance read latency and write consistency for optimal performance. Learn Cache-Aside, Write-Through, Write-Behind, and more.

- Repository: [Gaurav Kumar/system-design-notes](https://github.com/liquidslr/system-design-notes)
- Tags: deep-dive
- Published: 2026-09-11

---

**Choosing the right caching strategy requires balancing read latency against write consistency, where Cache-Aside offers simplicity but risks stale data, Write-Through guarantees consistency at the cost of slower writes, and Write-Behind maximizes throughput while accepting potential data loss during outages.**

The `liquidslr/system-design-notes` repository provides concrete implementations of caching patterns across real-world system designs, illustrating how the trade-offs between different caching strategies directly impact performance, consistency, and operational complexity. Understanding these patterns—from lazy loading to write-behind buffering—is essential for building scalable systems that handle frequently accessed data efficiently.

## Core Caching Pattern Trade-offs

### Cache-Aside (Lazy Loading)

**Cache-Aside** is the most common pattern where the application reads from the cache first; on a miss, it loads from the database and writes the result back to the cache.

As implemented in the Nearby Friends system (`17. Nearby Friends/Readme.md#L128-L143`), this pattern uses Redis to cache user locations with **TTL eviction**, demonstrating how rapidly changing data benefits from explicit cache management.

**Advantages:**
- Simple to implement with minimal infrastructure changes
- Guarantees fresh data after a cache miss
- Works well with read-heavy workloads

**Trade-offs:**
- First reads incur cache miss penalties
- Requires explicit cache-write logic in every code path
- Stale data can appear if the database updates without corresponding cache eviction

### Read-Through

**Read-Through** places the cache in front of the database, automatically loading data on a miss and abstracting loading logic from the application.

The Search Autocomplete design (`13. Search Autocomplete/Readme.md#L55-L59`) illustrates this pattern through a **Trie Cache** that lives in memory and falls back to the database if a term is missing, acting as a read-through layer for suggestion lookups.

**Advantages:**
- Removes cache-loading boilerplate from application code
- Guarantees cache population on reads

**Trade-offs:**
- Adds a layer of indirection (often requiring a dedicated cache service)
- May increase latency on cache misses compared to hand-rolled Cache-Aside approaches

### Write-Through

**Write-Through** sends writes to the cache first, which synchronously propagates them to the database before acknowledging success.

The Hotel Reservation System (`22. Hotel Reservation System/Readme.md#L390-L418`) highlights this pattern's consistency challenges when using Redis for room inventory, emphasizing the difficulty of keeping cache and database **consistent** during concurrent updates.

**Advantages:**
- Guarantees cache and database remain consistent (no stale reads)
- Simplifies read logic since the cache always holds the latest data

**Trade-offs:**
- Write latency includes full database write time
- Requires high cache availability; a cache outage blocks all writes

### Write-Behind (Write-Back)

**Write-Behind** applies writes to the cache and asynchronously persists them to the database, often batching operations for efficiency.

The Key-Value Store notes (`06. Key-Value Store/Readmi.md`) reference this pattern through **SSTable flushing**, where memory buffers persist to disk asynchronously.

**Advantages:**
- Fast write latency for clients (cache acknowledgment is immediate)
- Can batch database writes for higher throughput

**Trade-offs:**
- Risk of data loss if the cache crashes before flushing
- Stale reads possible until async flush completes
- Complex eviction and durability handling requirements

## Eviction and Expiration Strategies

### Time-Based Expiry (TTL)

**TTL (Time-To-Live)** expires cached entries after fixed intervals, preventing indefinite staleness.

The Hotel Reservation System (`22. Hotel Reservation System/Readme.md#L390-L418`) uses TTL to expire old room inventory data, balancing freshness against cache-hit rates.

**Trade-offs:**
- Simple to reason about and implement
- May evict still-valid data, wasting cache space
- Choosing the correct TTL requires balancing freshness against performance

### LRU and LFU Eviction

**LRU (Least Recently Used)** and **LFU (Least Frequently Used)** evict items when the cache reaches capacity, maximizing hit rates for hot data.

The Scaling chapter (`01. Scaling/Readme.md#L119-L121`) emphasizes **eviction policies** like LRU and discusses the need for **consistency** between cache and datastore when using these strategies.

**Trade-offs:**
- Adaptive to workload patterns, maximizing hit rates
- Requires bookkeeping overhead for tracking usage
- May evict expensive-to-recompute items if they fall outside recent usage windows

## Distributed and Edge Caching

### CDN Edge Caching

**CDN Edge Caching** stores static assets (JavaScript, CSS, images) on geographically distributed nodes.

**Trade-offs:**
- Reduces latency for end-users globally
- Offloads origin traffic for static content
- Not suitable for dynamic, user-specific data
- Invalidation can be slow, leading to stale assets

### Distributed In-Memory Cache (Redis)

**Distributed In-Memory Caches** like Redis use clustered key-value stores with sharding and replication.

**Trade-offs:**
- Low latency reads across many services
- Supports TTL and eviction policies out-of-the-box
- Requires operational overhead for replication and failover
- Memory is expensive; requires eviction strategies to stay within budget

## Implementation Examples

Below are Python implementations using `redis-py` that demonstrate the primary patterns found in the system design notes.

```python
import redis
import json
import time

r = redis.StrictRedis(host='localhost', port=6379, db=0)

# Cache-Aside (Lazy Loading) - As used in Nearby Friends

def get_user_profile(user_id):
    key = f"user:{user_id}"
    cached = r.get(key)
    if cached:
        return json.loads(cached)
    
    # Miss - fetch from DB

    profile = db_query_user(user_id)
    r.setex(key, 300, json.dumps(profile))  # 5-minute TTL

    return profile

# Write-Through - Ensures consistency like Hotel Reservation System

def update_user_profile(user_id, data):
    key = f"user:{user_id}"
    r.setex(key, 300, json.dumps(data))
    db_update_user(user_id, data)  # Synchronous persistence

# Write-Behind (Async flush) - Similar to SSTable flushing

WRITE_QUEUE = []

def update_user_profile_async(user_id, data):
    key = f"user:{user_id}"
    r.setex(key, 300, json.dumps(data))
    WRITE_QUEUE.append((user_id, data))

def background_flusher():
    while WRITE_QUEUE:
        uid, payload = WRITE_QUEUE.pop(0)
        db_update_user(uid, payload)
        time.sleep(0.01)

```

## Summary

- **Cache-Aside** provides the simplest implementation but requires manual cache management and risks stale data when the database updates directly.
- **Read-Through** abstracts caching logic from applications but adds infrastructure complexity and potential latency on misses.
- **Write-Through** guarantees strong consistency between cache and database at the expense of slower write latency and cache availability requirements.
- **Write-Behind** maximizes write throughput and reduces database load but introduces durability risks and consistency delays.
- **TTL eviction** offers predictable staleness windows, while **LRU/LFU** adapts to access patterns but requires additional memory overhead for tracking.
- **Distributed caches** like Redis and **CDN edge caches** solve different scale challenges but require significant operational investment for replication, failover, and invalidation.

## Frequently Asked Questions

### When should I choose Cache-Aside over Read-Through?

Choose **Cache-Aside** when you need fine-grained control over cache population logic or when working with existing applications where adding a dedicated cache layer is difficult. Choose **Read-Through** when you want to simplify application code by moving cache loading logic into the caching infrastructure itself, as demonstrated in the Search Autocomplete Trie Cache (`13. Search Autocomplete/Readme.md#L55-L59`).

### What are the primary risks of Write-Behind caching?

The main risks are **data loss** if the cache crashes before flushing to the database, and **stale reads** during the window between cache acknowledgment and database persistence. According to the Key-Value Store notes, mitigating these risks requires careful handling of durability and crash recovery mechanisms similar to SSTable flushing patterns.

### How do I decide between TTL and LRU eviction policies?

Use **TTL** for data with predictable expiration schedules, such as session tokens or time-sensitive inventory data like the Hotel Reservation System's room availability. Use **LRU** for workloads where access patterns determine value, ensuring hot data remains cached while cold data is evicted, as recommended in the Scaling chapter's discussion of eviction policies (`01. Scaling/Readme.md#L119-L121`).

### Can multiple caching strategies be combined in the same system?

Yes, systems often combine **CDN edge caching** for static assets with **Redis Cache-Aside** for dynamic data, or use **Write-Through** for critical consistency requirements while applying **Write-Behind** for non-critical analytics writes. The key is matching the consistency and latency requirements of each data type to the appropriate strategy.