Skip to main content

Dual Write

A dual write updates two systems in sequence with no atomic boundary, so a failure between writes leaves them silently inconsistent. Use the transactional outbox pattern or change-data-capture so one atomic write drives reliable propagation.

A dual write is when application code updates two systems — for example a database and a message broker, or a primary store and a search index — by writing to each one separately. The intent is to keep both in sync. The flaw is that there is no atomic boundary spanning both, so a crash or error after the first write and before the second leaves them inconsistent.

Why It Happens

Keeping a cache, search index, or downstream service current seems to require writing to it right after the database. The code reads naturally: save the record, then publish the event. Distributed transactions across heterogeneous systems are complex and discouraged, so teams skip coordination entirely and just do two writes in a row, assuming both will usually succeed.

Why It Hurts

The two writes are not atomic. If the process crashes, the network blips, or the second system is briefly down, you get one write without the other: an order saved but no event published, or an event published for a row that was rolled back. The stores diverge silently — no error necessarily fires — and the inconsistency persists until something downstream breaks or a reconciliation job notices. Retrying naively can double-apply one side. Under load these partial failures are not rare edge cases; they are guaranteed to happen eventually. The result is corrupted cross-system state that is hard to detect and harder to repair.

Warning Signs

  • Code performs a database write immediately followed by a separate publish or external call.
  • Two systems are kept in sync with no shared transaction or log to coordinate them.
  • The database and a cache, index, or downstream service periodically drift apart.
  • Incidents trace to an event that was never sent, or sent for data that never committed.

Better Alternatives

Use the transactional outbox pattern: write the business change and an outbox row in one local database transaction, then a separate relay reliably publishes the outbox entries. Because both writes share one transaction, they commit or roll back together, and the relay guarantees the event is eventually delivered. Alternatively, change-data-capture derives downstream updates from the database's commit log, so the database is the single source of truth and there is no second write to lose. For multi-step business processes across services, a saga coordinates with explicit compensating actions.

How to Refactor Out of It

Locate code paths that write to two systems sequentially. Introduce an outbox table written within the same transaction as the business change, and build or adopt a relay that publishes outbox rows with at-least-once delivery and idempotent consumers. Where appropriate, replace application publishing with CDC fed from the transaction log. Add reconciliation checks to detect and heal any historical drift. The goal is one atomic write plus reliable propagation, never two independent writes hoping both land.