Cache-aside vs. write-through vs. write-behind — how do these caching patterns actually differ?
Three ways to keep a cache and a database in sync, each trading off latency, consistency, and blast radius on failure differently.
LEARN BY DOINGPut your intuition to work3 steps
Cache-aside vs. write-through vs. write-behind — how do these caching patterns actually differ?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#The three shapes of "keep a cache in sync with a database"
Every caching strategy answers two questions: who is responsible for loading data into the cache, and who is responsible for writing changes through to the source of truth. The three canonical answers — cache-aside, write-through, and write-behind — sound like implementation trivia until a cache node dies mid-write and you have to explain to your team why some rows are stale, or gone.
#Cache-aside (a.k.a. lazy loading)
The application owns the caching logic explicitly. On a read, it checks the cache first; on a miss, it loads from the database and populates the cache for next time. On a write, the simplest version just writes to the database and invalidates (deletes) the cache key, letting the next read repopulate it.
def get_user(user_id):cached = redis.get(f"user:{user_id}")if cached is not None:return json.loads(cached)row = db.query("SELECT * FROM users WHERE id = %s", user_id)redis.setex(f"user:{user_id}", 300, json.dumps(row))return rowdef update_user(user_id, fields):db.execute("UPDATE users SET ... WHERE id = %s", user_id)redis.delete(f"user:{user_id}") # invalidate, don't update
Cache-aside is the default for a reason: the cache is disposable. If Redis falls over entirely, the app degrades to hitting the database directly (slower, but correct) rather than serving wrong data or crashing. Its weakness is the read-heavy stampede on a miss (many requests hit the DB at once) and the fact that writes leave a short window where the cache is empty rather than fresh — the next reader pays the cost of the reload.
#Write-through
Here the cache sits in the write path, not just the read path. The application writes to the cache, and the cache (or a thin wrapper around it) synchronously writes to the database before the call returns. Every write is immediately reflected in both places.
def update_user_write_through(user_id, fields):row = db.execute_and_return("UPDATE users SET ... WHERE id = %s RETURNING *", user_id)redis.setex(f"user:{user_id}", 300, json.dumps(row)) # cache is refreshed, not just invalidatedreturn row
Reads are always cache hits after the first write, and the cache never serves stale data relative to the last known write — assuming nothing else writes to the database directly. The cost is write latency: every write now pays for both the database round trip and the cache round trip, synchronously. It also means you're populating the cache with things that might never be read again, which can waste memory on write-heavy, read-cold data.
#Write-behind (write-back)
The application writes only to the cache, which acknowledges immediately, and a background process asynchronously flushes the change to the database — often batched, often via a queue.
def update_user_write_behind(user_id, fields):row = apply_fields_in_memory(user_id, fields)redis.setex(f"user:{user_id}", 300, json.dumps(row))write_queue.push({"op": "update_user", "id": user_id, "fields": fields})return row # caller sees success before the DB write happens# separate worker processdef flush_worker():batch = write_queue.pop_batch(max_size=100)db.execute_many("UPDATE users SET ... WHERE id = %s", batch)
Write latency drops to "however fast the cache is," and batching many writes into fewer database round trips can dramatically cut database load — this is how systems absorb write spikes. The trade-off is durability: if the cache node or the queue dies before the flush runs, that write is gone, and the database silently disagrees with what every client was told succeeded.
#Comparing the three
| Cache-aside | Write-through | Write-behind | |
|---|---|---|---|
| Who loads on read miss | Application | Application (rare, cache stays warm) | Application |
| Who writes to DB | Application, directly | Cache layer, synchronously | Background worker, asynchronously |
| Write latency | DB-only | DB + cache (higher) | Cache-only (lowest) |
| Risk on cache failure | Falls back to DB, slower | Falls back to DB, slower | Data loss for unflushed writes |
| Best for | General-purpose reads | Read-after-write consistency | Very high write throughput |
Most production systems don't pick one pattern globally — they pick per data type. Cache-aside for the bulk of reads, write-through for data that must never look stale right after a write (e.g., a user's own profile, immediately after they save it), and write-behind reserved for high-volume, loss-tolerant writes like view counters or activity logs, where losing the last few increments is an acceptable trade for absorbing a traffic spike.