Caching Strategies: Cache-Aside, Write-Through, and When to Use Each
A systematic breakdown of caching patterns, their consistency guarantees, and the failure modes you need to plan for.
Caching is where developers most often introduce subtle bugs. The right strategy depends on your consistency requirements, read/write ratio, and tolerance for stale data.
The Fundamental Trade-off
Every cache is a promise: “this data is close to what’s in the source of truth.” How close depends on your invalidation strategy.
Cache-Aside (Lazy Loading)
The application manages the cache explicitly. On a cache miss, the application fetches from the database and populates the cache.
def get_user(user_id: str) -> User:
cached = cache.get(f"user:{user_id}")
if cached:
return deserialize(cached)
# Cache miss — fetch from DB
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
cache.set(f"user:{user_id}", serialize(user), ttl=300)
return user
Pros: Only cache what’s actually needed. Cache failures degrade gracefully (fall through to DB).
Cons: Cache miss penalty (2 round trips). Thundering herd on cold start. Data can be stale until TTL expires.
Write-Through
On every write, update both the cache and the database synchronously.
def update_user(user_id: str, data: dict) -> User:
user = db.update("UPDATE users SET ... WHERE id = ?", user_id, data)
cache.set(f"user:{user_id}", serialize(user), ttl=300)
return user
Pros: Cache is always fresh. No cold-start penalty for hot paths.
Cons: Write latency increases (two writes). Cache fills with data that may never be read.
Write-Behind (Write-Back)
Write to cache immediately, persist to database asynchronously. High risk, high performance.
Use only when: You can tolerate data loss on cache failure. Gaming leaderboards, analytics counters, and similar use cases where approximate accuracy is acceptable.
TTL vs Event-Based Invalidation
TTL-based expiry is simple but means you’re always serving data that’s at most N seconds stale.
Event-based invalidation is precise but complex: when a user updates their profile, publish an event that invalidates user:{id} in every cache layer.
The Thundering Herd Problem
When a popular cache key expires, hundreds of concurrent requests hit the database simultaneously. Mitigations:
- Probabilistic early expiration — randomly refresh slightly before TTL
- Cache locking — first miss acquires a lock and refreshes; others wait
- Stale-while-revalidate — serve stale data while refreshing in the background
Key Decision Matrix
| Strategy | Consistency | Read Perf | Write Perf | Complexity |
|---|---|---|---|---|
| Cache-Aside | Eventual (TTL) | Good after warm | Unaffected | Low |
| Write-Through | Strong | Excellent | Slower | Medium |
| Write-Behind | Eventual | Excellent | Fast | High |
| Read-Through | Eventual (TTL) | Good | Unaffected | Low |
Pick the simplest strategy that meets your consistency requirements. Cache-aside handles 80% of use cases.