Stackbook Logo
messagingestablished · medium operational burden

Dual Writes Problem

Also known as: dual write anti-pattern, two-system write inconsistency

Intent

Avoid inconsistency caused by updating a database and publishing a message as two separate, non-atomic steps.

Problem

A service saves to its database and then publishes an event. If it crashes or the broker is down between the two, the database changed but no event was sent, or the reverse. Retrying code cannot fix a lost process.

Forces

  • •No transaction spans database and broker
  • •Failures can occur between any two steps
  • •Ordering of the two writes changes the failure mode
  • •Downstream systems depend on the event

Solution

Overview

The dual writes problem appears whenever code does save(entity) followed by publish(event). Each step can fail independently, and there is no transaction coordinating them.

Swapping the order only changes the failure: lose the event, or emit an event for a change that never happened.

It is worth spotting in the wild. Typical shapes include updating a database and then a search index, saving an order and then sending an email, or writing a database row and then invalidating a cache. Each looks harmless in a happy-path test and fails only under crashes, timeouts or deploys, which makes it a persistent source of data drift.

How it works

The fix is to make one write the source of truth and derive the other. With an outbox, the event row commits atomically with the state, and a relay publishes it.

-- broken
db.save(order)        # crash here...
broker.publish(evt)   # ...event lost

-- fixed
BEGIN; INSERT orders ...; INSERT outbox(evt); COMMIT;
relay: read outbox -> publish -> mark sent

The two main cures differ in where capture happens. A transactional outbox captures events in application code inside the same transaction. Change data capture tails the database write-ahead log and requires no application changes, but couples consumers to table structure unless you add an outbox table anyway. Either way, the relay is at-least-once, so downstream handling must tolerate duplicates.

Pitfalls

  • Wrapping both calls in try and catch with retries, which cannot survive a process crash.
  • Using two-phase commit across heterogeneous systems that do not support it.
  • Publishing inside the database transaction, then rolling back.
  • Skipping consumer idempotency after adding a relay.

Key takeaways

  • Two independent writes cannot be made atomic by careful code.
  • Make one write authoritative and derive the rest.
  • Use outbox or change data capture.
  • Pair with idempotent consumers and reconciliation.

✓ When to Use

  • •Recognize it during design reviews of any save-then-publish code
  • •State must be reflected in both a database and a broker or cache
  • •Search index or cache updates follow database writes

✗ When Not to Use

  • •A single datastore handles both state and events
  • •Losing occasional updates is acceptable and repaired by periodic sync
  • •Both effects are idempotent and retried by a durable workflow

Pros

  • +Naming the problem prevents a common bug class
  • +Fixes lead to atomic and reliable event emission
  • +Makes reconciliation needs explicit

Cons

  • −Fixes add components such as relays or CDC
  • −Eventual rather than immediate downstream consistency
  • −Idempotent consumers become mandatory

Cost Profile

Infrastructure

Medium - outbox relay or CDC

Operational

Medium - relay lag and cleanup

Cognitive

Medium - understanding failure windows

Failure Modes

  • Commit succeeds but publish fails leaving consumers unaware

  • Publish succeeds but commit rolls back, emitting a phantom event

  • Concurrent writers apply updates to the two systems in different orders

Real-World Examples

Alternatives

    Related Patterns

    • outbox-pattern
    • transactional-inbox
    • exactly-once-processing

    Competency Domains

    data statereliability ops