Q18 — What is cache-aside, write-through, and write-behind — why does the choice matter?
Why does the caching strategy matter at all?
Once you decide to add a cache, you face a question that's easy to overlook: exactly when and how do you synchronize the cache with the database?
The answer isn't neutral. Different strategies make different promises about consistency, performance, and failure behavior. Picking the wrong one for your workload means either stale data in production, unexpected write latency, or data loss when a cache node dies. The strategy is a fundamental design decision, not a configuration detail.
Cache-aside (lazy loading)
Cache-aside is the most common strategy. The application itself manages the cache explicitly. The cache and database are not directly connected — the application decides what to load, when, and how.
Read path:
- Application checks the cache for the key
- Cache hit → return immediately, done
- Cache miss → application fetches from the database, writes the result into the cache, returns the data
Write path:
- Application writes to the database
- Application invalidates (deletes) the corresponding cache key
The cache is populated lazily — only when data is actually requested. If nobody ever reads user 999's profile, it never enters the cache. Memory is used only for data that's actually needed.
Why the write path invalidates rather than updates: If you update the cache with the new value on every write, concurrent writes from multiple servers can race and leave the cache with a stale value. Deleting the key on write forces the next read to re-populate from the database, which is always the authoritative source. This is safer under concurrency.
The first-request penalty: On a cache miss, you pay three hops — check cache (miss), read database, write to cache — before returning to the user. The first request after a cold start or invalidation pays this penalty. For most applications this is acceptable; for latency-critical paths it might not be.
The staleness window: Between the database write and the cache invalidation, the cache holds the old value. If the invalidation fails (a network hiccup to Redis), the stale value persists until the TTL expires. This window is usually milliseconds to seconds — acceptable for most data, not acceptable for security-critical data like permissions and session tokens.
When cache-aside is the right choice: Most read-heavy web applications. It's resilient — if the cache goes down entirely, the application falls back to the database without any code changes. The cache is purely an optimization, not a dependency.
Write-through
In write-through, every write to the database is also written to the cache, synchronously, before acknowledging success to the client. The cache and database are always in sync.
Write path:
- Application writes to cache (synchronously)
- Cache writes to database (synchronously)
- Both confirmed → return success to client
Or in the application-managed variant:
- Application writes to database
- Application writes same value to cache
- Return success
The key guarantee: After a write, the cache always has fresh data. Any subsequent read — even microseconds later — hits the cache and gets the new value. No staleness window.
The write cost: Every write pays the cache write in addition to the database write. The round trip to Redis adds latency to every write. For write-heavy applications, this additional latency accumulates. A service that writes thousands of records per second is making thousands of extra round trips to Redis per second.
The cache bloat problem: Write-through populates the cache on every write, regardless of whether that data will ever be read. A batch job that inserts a million records will populate the cache with a million entries, most of which are never read, evicting hot data that was actually useful. This is "cache pollution" — filling memory with data that doesn't benefit read performance.
Write-through makes sense when reads consistently follow writes for the same data — user profile updates, settings changes, anything where the user immediately reads back what they just wrote. It makes less sense for bulk imports or event logs where writes rarely lead to immediate reads.
Write-behind (write-back)
Write-behind pushes consistency to the extreme: acknowledge the write immediately after writing to the cache, and asynchronously persist to the database later. The cache is written first; the database catches up.
Write path:
- Application writes to cache
- Immediately return success to client
- Background worker drains the cache writes to the database asynchronously
Why this is fast: The client doesn't wait for the database write. Database writes, especially under load with replication and durability guarantees, can take milliseconds. Write-behind makes them invisible to the user.
The durability risk: If the cache node dies before the background worker has persisted the writes to the database, those writes are permanently lost. The client was told "success," the user believes their data is saved, and it isn't. For financial data, user-generated content, or anything where data loss is unacceptable, this is catastrophic.
The complexity cost: Write-behind requires a reliable queue between the cache and the database, retry logic for failed database writes, ordering guarantees (writes to the same key must reach the database in order), and a strategy for what to do if the database is down for an extended period. What seemed like a simple optimization becomes a small distributed system.
When write-behind is appropriate: Extremely write-heavy, loss-tolerant workloads. Analytics event counters (losing a few counts is acceptable). Metrics aggregation. Any scenario where the write throughput is genuinely the bottleneck and some data loss is explicitly acceptable. This is a narrow use case — most production systems should not use write-behind without careful deliberation about the failure modes.
The refresh-ahead pattern
A fourth strategy worth knowing: the cache pre-emptively refreshes data before it expires, rather than waiting for a miss.
For data with predictable access patterns (a popular news article, a user's home feed), the cache can detect that a key is approaching its TTL and trigger a background refresh. The key is updated before it expires — users never see a cache miss.
The cost: you're fetching data that might not be needed (if the item stopped being popular). And predicting which items will be accessed after their TTL requires either heuristics or background knowledge about access patterns.
Refresh-ahead is common in CDNs and media platforms where popular content is accessed constantly and a cache miss would be very visible. It's less common in application-layer caches.
How to choose
The right strategy depends on the nature of your data and what failures you can tolerate:
Cache-aside: Default choice for most applications. Read-heavy, writes are infrequent enough that first-read penalty and short staleness window are acceptable. Cache is an optimization, not a critical path.
Write-through: When reads immediately follow writes and consistency matters more than write latency. User-facing data that the user sees right after saving. Small write volumes.
Write-behind: When write throughput is the primary bottleneck and some data loss on cache failure is explicitly acceptable. Narrow use case. Requires significant operational complexity.
The mental model
Cache-aside (lazy):
Read: cache? → hit: return | miss: DB → cache → return
Write: DB → invalidate cache key
Pro: cache is optional, resilient, only caches what's read
Con: first-read penalty, short staleness window on writes
Write-through:
Read: cache → always fresh
Write: DB + cache simultaneously, synchronously
Pro: cache always consistent with DB
Con: write latency (extra cache round trip), cache polluted by write-only data
Write-behind:
Read: cache (always populated)
Write: cache immediately → DB asynchronously later
Pro: fastest writes possible
Con: data loss if cache dies before async flush, complex to operate correctly
Choosing:
Read-heavy, tolerates short staleness → cache-aside
Write → immediate read, consistency matters → write-through
Write-heavy, loss-tolerant (rare) → write-behind
Every strategy is a trade-off between consistency, write latency, and complexity. The choice should be driven by what your data means to your users — not by what's easiest to implement.