advanced 3 min answer

A service must update its database and publish an event, and both must happen or neither. Why is a dual write wrong, and how do the outbox pattern and log-based capture compare?

outboxcdcdual-writeatomicitymessaging
Show the full answer Hide the answer

Why the dual write is wrong

The obvious implementation — commit the transaction, then publish the event — has a window between the two. If the process dies in that window, the database is updated and no event exists, permanently. The downstream system never learns, and nothing detects it, because from every component's perspective its own operation succeeded.

Reversing the order is worse: publish then commit can emit an event for a transaction that then rolls back, so downstream systems act on something that never happened.

Wrapping both in a distributed transaction across a database and a broker is possible in principle and is operationally unattractive — it couples their availability, it is slow, and most modern brokers do not support it.

There is no ordering of two independent writes that is atomic. The problem is structural, not a matter of care.

The outbox pattern

Write the event into a table in the same database, in the same transaction as the business change. Atomicity is now guaranteed by the database, which is the one thing that can guarantee it.

A separate process then reads unpublished rows from that table, publishes them to the broker, and marks them sent.

  • The event exists if and only if the business change committed.
  • Publication is at-least-once: a crash after publishing and before marking produces a duplicate. Consumers must deduplicate on an event identifier, and this must be stated in the contract rather than assumed.
  • Ordering is preserved by publishing in insertion order, per key at minimum.
  • The outbox table needs cleanup, or it grows without bound.

Log-based capture

Rather than a poller reading the outbox table, a connector reads the database's replication log and publishes from there.

  • Lower latency and no polling load on the database.
  • It sees every committed write, including those made by migration scripts, admin tools and other services — which application-level publishing cannot.
  • Costs: a replication connector to operate, and coupling to the database's internal change format.

Publishing raw table changes to consumers is a mistake, however. It exposes the internal schema as a public contract, so every column rename becomes a breaking change for every consumer. The outbox table remains the right target even with log-based capture: the outbox row is a deliberately-designed event, and the log is merely the transport that carries it out of the database.

The combination that is usually right

Outbox table for the contract, log-based capture for the transport. The application writes a well-designed event into the outbox within its transaction; a connector tails the log and publishes it. This gets atomicity from the database, low latency from the log, and a stable public contract from the outbox schema.

What still requires reconciliation

At-least-once delivery means duplicates, so consumers deduplicate or are idempotent. Ordering across different keys is not guaranteed, so consumers must not assume global order. And a consumer that processes an event and then fails before recording that it did will process it again — which is why the effect and the record of the effect must be in one transaction.

Exactly-once end-to-end is achieved by at-least-once delivery plus idempotent handling, and there is no mechanism that avoids the second half.