← Concepts

阅读中文版本

The Dual Write Problem

A dual write updates two systems in one code path — database plus cache, database plus queue — and any crash between them leaves permanent divergence no retry can detect.

architecture

Definition

The dual write problem arises when a transaction must modify two separate systems with no shared atomicity: writing the database then publishing to the broker, or writing the cache then the store. A failure between the steps desynchronizes them, and neither system records that the other was missed. Solutions are the transactional outbox (state plus event in one database transaction), CDC from the database, or idempotent reconciliation.

Context and scope

Appears wherever caches, search indexes, and message queues are updated alongside primary stores. The root cause of phantom inconsistencies in service code.

Common misconceptions

  • Wrapping both writes in retry logic fixes it — retries re-execute the succeeded step too, creating duplicates or divergence in the other direction
  • A distributed transaction fixes it — 2PC does provide atomicity, at availability and throughput costs most systems refuse; the outbox avoids both
  • eventual consistency makes it harmless — eventual convergence requires a source of truth to converge toward; dual writes leave no record of what should have happened

Anti-patterns

  • Writing the cache after the database commit without a tombstone or TTL, shipping stale-on-crash forever
  • Publishing events after commit and treating lost publishes as acceptable noise until reconciliation discovers them
  • Calling a search index update inside the DB transaction and holding locks on remote calls

Related concepts

Authoritative source

https://www.confluent.io/blog/dual-write-problem/

What it is

Two systems, one logical change, no shared transaction. The code between the two writes is a window where any crash leaves half the change done — and no log anywhere says which half.

How it works

Failure modes split: DB committed, publish lost — downstream never learns; publish sent, DB rolled back — downstream learned a lie. Both are silent. The outbox pattern makes the event a row in the same transaction as the state, with a relay publishing asynchronously and retrying to delivery. CDC variants read the WAL instead of an outbox table, making the database itself the source of events.

When to eliminate

Whenever secondary stores must track the primary: caches, search indexes, analytics events, webhook fan-outs. Treat any pair of mutating calls in sequence as a dual write audit finding.

Pitfalls

The relay must be at-least-once with consumer idempotency — exactly-once delivery is not achievable end to end. Cache-side patterns dodge dual writes by accepting TTL staleness instead of pretending atomic invalidation. Async publication via outbox changes read-your-writes behavior: request-scoped reads need a session pin or synchronous fallback.