A service must update its database and publish an event. Why is doing both directly incorrect, and what does the outbox pattern guarantee?
Show the full answer Hide the answer
Why the direct approach is incorrect
The two writes are not atomic. Whatever order they occur in, a failure between them leaves the systems inconsistent:
- Database first, then publish. The service crashes after committing and before publishing. The state changed and nobody was told — a message is recorded as sent and no delivery is triggered.
- Publish first, then database. The service crashes after publishing and before committing. Consumers act on an event describing a state change that did not happen.
Neither is recoverable by retry, because the service does not know which side succeeded. And crucially, the inconsistency is silent — nothing errors, and the divergence is discovered later by a customer or by reconciliation.
What the outbox guarantees
The event is written to a table in the same local transaction as the state change. Either both commit or neither does. A separate process reads the outbox and publishes.
That gives: if the state changed, the event will be published.
What it does not guarantee
Exactly-once delivery. The publisher may crash after publishing and before marking the row as sent, so the event is published again. Delivery is at-least-once, which forces the complementary requirement: consumers must be idempotent, keyed on a business identifier or an event id.
Attempting exactly-once across a network to systems you do not control is not achievable, and designing for at-least-once with idempotent consumers is the correct response rather than a compromise.
The operational details that matter
- Ordering, if required, comes from publishing in outbox insertion order per key — not globally.
- Outbox growth needs a cleanup process, or the table becomes the largest in the database.
- Publisher lag monitored, since a stalled publisher means state changes that nobody hears about, with no error anywhere.
- Change data capture can replace the polling publisher, reading the database log directly — lower latency and no query load, at the cost of another component.
The alternative worth considering
Do not publish at all where a database-backed queue suffices. At moderate volume, enqueueing work in the same database and transaction removes the dual-write problem entirely rather than mitigating it — no outbox, no broker, no reconciliation. The outbox exists because an external broker was chosen; if that choice is not required, the problem does not arise.