Trade-offs Between Different Caching Strategies: 8 Patterns Explained

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.

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →