The Dual-Write Bug & CDC
Writing to a database and a cache as two separate steps looks fine — until the process crashes between them.
Overview
The dual-write bug is what happens when application code writes to a database and a cache as two separate, non-atomic steps — if the process crashes, times out, or races with another write between those two steps, the cache silently drifts out of sync with the database, with no error raised anywhere.
Why It Exists
It's tempting to write `db.save(order); cache.set(order.id, order)` right in the request handler — it looks correct, and it works in every manual test. The dual-write bug exists precisely because these two writes are never actually atomic: any crash, timeout, retry, or concurrent request landing between them leaves the cache holding stale data that nothing will ever correct on its own.
Real World Example
A checkout service updates an order's status to 'shipped' in Postgres, then updates the same order in Redis. The deploy pipeline restarts the service between those two lines during a rolling deployment. The database says 'shipped'; the cache still says 'processing' — and because cache reads are what the customer-facing app actually serves, the customer sees a stale status indefinitely, until the cache entry happens to expire.
Example Data
The same two writes, two different outcomes
| Scenario | Database | Cache | Result |
|---|---|---|---|
| Normal path | shipped | shipped | Consistent |
| Crash between writes | shipped | processing (stale) | Silently inconsistent |
The Bug, and the Fix
Why Dual Writes Aren't Atomic
Writing to two different systems from application code means two separate network calls, two separate points of failure, and no shared transaction spanning both — there's no way to guarantee both writes happen, or neither does.
Why It's Silent
Neither system raises an error when this happens — the database write succeeded, and the cache write either succeeded with stale data or failed independently. Nothing detects or repairs the divergence automatically.
Change Data Capture (CDC) — Reading From the Source of Truth
CDC tools like Debezium read the database's write-ahead log directly and stream every committed change to downstream consumers, including the cache — removing the second, independent write from application code entirely.
Why CDC Actually Fixes It
Because CDC reads only from the database's committed log, the cache is always updated from what the database actually persisted — there's no code path where the cache can diverge from a write the database never committed.
Diagram
Dual write (two independent points of failure) vs. CDC (single source of truth)
App writes to DB
step 1
App writes to cache
step 2 — can fail independently
CDC instead: App writes to DB only
Debezium streams the change to the cache
Common Mistakes
Writing to the database and the cache from the same request handler and assuming both will succeed
Why: Any crash, timeout, or deployment restart between the two calls leaves the cache stale with no error surfaced anywhere in the system.
Fix: Use CDC to update the cache from the database's committed write-ahead log instead of writing to the cache directly from application code.
Trying to 'fix' dual writes by wrapping both calls in a try/catch and retrying on failure
Why: A retry after a partial failure can't tell whether the first write actually succeeded or not, and re-running both writes risks double-applying one of them.
Fix: Remove the second, independent write entirely rather than trying to make an inherently non-atomic operation reliable through retries.
Assuming a short cache TTL 'solves' dual-write staleness
Why: A TTL bounds how long the staleness lasts, but doesn't prevent it — for the duration of the TTL, incorrect data is still being served as if it were current.
Fix: Treat CDC (or cache invalidation triggered by the same transaction) as the actual fix, and use TTLs only as an unrelated, additional safety net.
Interview Questions
What is the dual-write bug?
It's the data-corruption risk that comes from writing to a database and a cache as two separate, non-atomic steps in application code — if the process fails between the two writes, the cache silently ends up holding stale data.
Why doesn't wrapping both writes in a try/catch block with a retry fix the dual-write bug?
A retry can't distinguish a fully-failed attempt from a partial failure where the first write already succeeded — retrying risks re-applying the database write, or leaves an ambiguous window where you can't tell what state the cache is actually in relative to the database.
How does Change Data Capture eliminate the dual-write bug at an architectural level, rather than just reducing its likelihood?
CDC removes the second, independent write from application code entirely — instead of the app writing to the cache directly, a tool like Debezium tails the database's write-ahead log and streams every committed change downstream. Because the cache update is now derived from what the database actually committed, rather than from a second best-effort write, there's no code path left where the two can diverge — it's a structural fix, not a probabilistic one.
Production Best Practices
Do
✓Use CDC to derive cache updates from the database's committed write-ahead log.
✓Treat any independent, uncoordinated write to a second system as a potential source of silent drift.
✓Use cache TTLs as an additional safety net, not as the primary consistency mechanism.
Don't
✗Don't write to a database and a cache as two independent, non-atomic steps in application code.
✗Don't try to fix dual writes with retries — the ambiguity of a partial failure makes retries unsafe.
✗Don't assume a short TTL prevents incorrect data from being served, only that it bounds how long it lasts.
Comparison
| Source of Truth | Failure Mode | Consistency Guarantee | |
|---|---|---|---|
| Dual Write | Two independent writes | Silent divergence on partial failure | None |
| CDC | Database's write-ahead log | Cache always reflects committed writes | Eventually consistent, but never silently wrong |