Skip to content
Low Level Design Mastery Logo
LowLevelDesign Mastery

Caching Fundamentals

Speed is a feature - caching makes it possible

Imagine you’re a librarian. Every time someone asks for a book, you could walk to the massive warehouse (database) to find it. Or, you could keep the 100 most popular books on a cart right next to you (cache). When someone asks for a popular book, you grab it instantly. That’s caching.

Caching is storing frequently accessed data in fast storage (usually memory) to avoid slow operations like database queries or external API calls.

Diagram
ProblemHow Caching Solves It
Slow Database QueriesCache stores results, avoiding repeated queries
High Database LoadReduces database requests by 90%+
Expensive External APIsCache API responses, avoid rate limits
Repeated ComputationsCache expensive calculation results
Geographic LatencyCache data closer to users (CDN)

There are four main ways to integrate caching into your application. Each has different trade-offs.

The most common pattern. Your application manages the cache directly.

Diagram

How it works:

  1. Application checks cache first
  2. If cache hit → return data immediately
  3. If cache miss → fetch from database
  4. Store result in cache for next time
  5. Return data to user

When to use:

  • ✅ Most common use case
  • ✅ You want full control over cache logic
  • ✅ Different data needs different caching strategies
  • ✅ Cache and database can be separate systems

Trade-offs:

  • ✅ Simple to understand and implement
  • ✅ Flexible - you control everything
  • ❌ Application code must handle cache logic
  • ❌ Risk of cache stampede (many requests on miss)

The cache acts as a proxy. Your application only talks to the cache; the cache handles database access.

Diagram

How it works:

  1. Application requests data from cache
  2. Cache checks if data exists
  3. If miss, cache automatically fetches from database
  4. Cache stores the result
  5. Cache returns data to application

When to use:

  • ✅ You want simpler application code
  • ✅ Cache library handles database complexity
  • ✅ Consistent caching behavior across application
  • ✅ Good for read-heavy workloads

Trade-offs:

  • ✅ Simpler application code
  • ✅ Cache handles all complexity
  • ❌ Less flexible than cache-aside
  • ❌ Cache library must support your database

Writes go to both cache and database simultaneously. Ensures they stay in sync.

Diagram

How it works:

  1. Application writes data
  2. Cache updates immediately
  3. Cache writes to database simultaneously
  4. Wait for both to complete
  5. Return success

When to use:

  • ✅ Strong consistency required
  • ✅ Can’t afford stale cache data
  • ✅ Write latency is acceptable
  • ✅ Critical data that must be accurate

Trade-offs:

  • ✅ Strong consistency
  • ✅ Cache and database always match
  • ❌ Higher write latency (waits for DB)
  • ❌ Database becomes bottleneck for writes

Write to cache immediately, database write happens later. Fastest writes, but risky.

Diagram

How it works:

  1. Application writes data
  2. Cache updates immediately
  3. Return success to application (fast!)
  4. Queue database write for later
  5. Background process writes to database

When to use:

  • ✅ Write performance is critical
  • ✅ Can tolerate eventual consistency
  • ✅ Non-critical data (analytics, logs)
  • ✅ High write volume

Trade-offs:

  • ✅ Lowest write latency
  • ✅ High write throughput
  • ❌ Risk of data loss
  • ❌ Eventual consistency (cache and DB may differ temporarily)

LLD Connection: Implementing Cache Patterns

Section titled “LLD Connection: Implementing Cache Patterns”

At the code level, caching patterns translate to decorator patterns and repository abstractions.

The decorator pattern is perfect for adding caching to existing repositories:

cache_decorator.py
from functools import wraps
from typing import Callable, Any
import time
class CacheDecorator:
def __init__(self, cache: dict, ttl: int = 300):
self.cache = cache
self.ttl = ttl # Time to live in seconds
def __call__(self, func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
# Create cache key from function args
cache_key = f"{func.__name__}:{args}:{kwargs}"
# Check cache (cache-aside pattern)
if cache_key in self.cache:
cached_data, timestamp = self.cache[cache_key]
if time.time() - timestamp < self.ttl:
return cached_data
# Cache miss - fetch from source
result = func(*args, **kwargs)
# Store in cache
self.cache[cache_key] = (result, time.time())
return result
return wrapper
# Usage
cache = {}
@CacheDecorator(cache, ttl=300)
def get_user(user_id: int):
# Simulate database query
return {"id": user_id, "name": "John"}

A more complete example showing cache-aside in a repository:

user_repository.py
from abc import ABC, abstractmethod
from typing import Optional
class UserRepository(ABC):
@abstractmethod
def get_user(self, user_id: int) -> Optional[dict]:
pass
class DatabaseUserRepository(UserRepository):
def get_user(self, user_id: int) -> Optional[dict]:
# Simulate database query
return {"id": user_id, "name": "John"}
class CachedUserRepository(UserRepository):
def __init__(self, db_repo: UserRepository, cache: dict):
self.db_repo = db_repo
self.cache = cache
def get_user(self, user_id: int) -> Optional[dict]:
# Cache-aside pattern
cache_key = f"user:{user_id}"
# Check cache first
if cache_key in self.cache:
return self.cache[cache_key]
# Cache miss - fetch from DB
user = self.db_repo.get_user(user_id)
# Store in cache
if user:
self.cache[cache_key] = user
return user

PatternRead LatencyWrite LatencyConsistencyComplexityUse Case
Cache-AsideLow (cache hit)LowEventualMediumMost applications
Read-ThroughLow (cache hit)LowEventualLowRead-heavy apps
Write-ThroughLow (cache hit)High (waits for DB)StrongMediumCritical data
Write-BehindLow (cache hit)Very LowEventualHighHigh write volume

🎯 Cache-Aside is King

Most applications use cache-aside. It’s flexible, understandable, and gives you control.

⚡ Speed Matters

Cache lookups are 100x faster than database queries. At scale, this difference is massive.

🔄 Consistency Trade-offs

Faster writes (write-behind) = weaker consistency. Stronger consistency (write-through) = slower writes.

🏗️ Decorator Pattern

Use decorator pattern in code to add caching transparently to existing repositories.