
Every cache forces you to answer one question: when data changes, who updates the cache, and when? Reads are the easy half. The write path is where caches go stale, lose data, or slow your app down.
Three patterns cover most real-world systems. In each, the app talks to a cache (Redis, Valkey, Memcached) that sits in front of a database.
1. Cache-aside (lazy loading)
The app owns the logic. On a read, it checks the cache first. On a miss, it loads from the database, stores the result in the cache with a TTL, and returns it.
sequenceDiagram
participant App
participant Cache
participant DB
App->>Cache: GET user:42
alt cache hit
Cache-->>App: value
else cache miss
Cache-->>App: nil
App->>DB: SELECT user 42
DB-->>App: row
App->>Cache: SET user:42 with TTL
endOn a write, the app updates the database and then deletes the cached key. The next read repopulates it.
Strengths
Simple, and it works with any cache.
Only data that is actually requested gets cached.
If the cache goes down, the app still works, just slower, as long as the database can absorb the full read load.
Watch out for
The first request after a cache miss is slower, and a cold cache can cause many misses at once.
There is a window of staleness between a database write and the cache refresh. A race is possible too: a reader fetches the old row, a writer updates the database and deletes the key, then the reader writes the old value back. A short TTL bounds how long the stale value survives. For stricter guarantees, use versioned values, short-lived locks (leases) on the miss path, or a delayed second delete after the write.
Popular keys expiring together can cause a thundering herd. Add jitter to TTLs and coalesce concurrent loads for the same key.
2. Write-through
Every write goes through the cache, and the cache writes to the database synchronously before acknowledging. Reads of recently written data then find fresh values in the cache.
sequenceDiagram
participant App
participant Cache
participant DB
App->>Cache: SET user:42 new value
Cache->>DB: write user 42
DB-->>Cache: ok
Cache-->>App: okStrengths
The cache and database agree after a successful write, unless a partial failure or concurrent writer interferes.
The read path is trivial, and hit rates are high for data that has been written.
Watch out for
Write latency is the cache write plus the database write.
You cache data that may never be read again, which wastes memory.
If the database write fails, you must undo or skip the cache update.
Two concurrent writers to the same key can interleave so the cache and database end up with different values. Serialize writes per key or attach a version number.
Write-through is usually paired with a read-through layer, where the cache itself loads missing data, so the app only ever talks to the cache. Redis and Valkey don't do this on their own, so in practice it lives in your application code or a caching library.
3. Write-behind (write-back)
The app writes to the cache, gets an immediate acknowledgment, and the cache flushes changes to the database asynchronously, often in batches.
sequenceDiagram
participant App
participant Cache
participant Buffer as Write buffer
participant DB
App->>Cache: SET user:42 new value
Cache-->>App: ok (fast)
Cache->>Buffer: enqueue change
Note over Buffer,DB: flushed later in batches
Buffer->>DB: batch write
DB-->>Buffer: okStrengths
It has the lowest write latency of the three.
Multiple updates to the same key can be coalesced into one database write.
It absorbs write spikes and reduces database load.
Watch out for
If an acknowledged write exists only in volatile memory and the cache or buffer fails before it is flushed, that write is lost. Durable queues and replication reduce this risk, but they add complexity and don't eliminate it.
The database lags behind the cache, so anything reading the database directly (reports, other services) sees stale data.
Retries, ordering, and failure handling add real complexity.
This fits data where losing a few seconds is acceptable, such as view counters, activity logs, and telemetry. It is a poor fit for payments or inventory.
Side-by-side
Cache-aside | Write-through | Write-behind | |
|---|---|---|---|
Who manages the cache | App | Cache layer or app library | Cache layer or app library |
Write latency | Database + a cache delete | Cache + database | Cache only |
Freshness after a write | Fresh after invalidation; stale if an invalidation is missed or raced (TTL bounds it) | Fresh after both updates succeed | Fresh in cache, stale in database |
Data loss risk | None | None | Yes, if the cache fails before a flush (durable buffers reduce this) |
Complexity | Low | Medium | High |
Best for | Read-heavy workloads | Read-heavy data that must stay consistent | Write-heavy, loss-tolerant data |
Choosing one
flowchart TD
A["Are writes heavy, and can you tolerate delayed database updates and losing unflushed writes?"]
A -->|Yes| WB["Write-behind"]
A -->|No| B["Do you need stronger freshness than invalidate-on-write gives?"]
B -->|Yes| WT["Write-through"]
B -->|"No, brief staleness is fine"| CA["Cache-aside"]Practical tips
Default to cache-aside. It is the simplest and the most forgiving when something fails.
Always set a TTL as a safety net, even when you invalidate explicitly. It limits how long a bug or a missed invalidation can hurt you. With write-through, make sure an expired key can be reloaded from the database.
For cache-aside, delete on write rather than update. It stops concurrent writers from overwriting each other with out-of-order values. It doesn't stop a slow reader from caching an old value, so pair it with a TTL, and with versioning or leases if staleness is costly. Write-through and write-behind update the cache by design.
Mix patterns. Many systems use cache-aside for most data, write-through for a small set of hot, critical records, and write-behind for counters.
Watch your hit ratio and your database load. A cache that isn't absorbing reads is just extra latency.
Wrapping up
None of these patterns is "best." Each trades freshness, latency, and durability against complexity. Start with cache-aside, move to write-through where staleness hurts, and reach for write-behind only when write volume demands it and you can live with the risk.
Comments(0)
Join the discussion by logging into your account.
No comments yet. Be the first to comment!