The outbox pattern (also called the transactional outbox) guarantees that a service publishes an event whenever it changes its own state, without losing either side to a partial failure. The event is written to an outbox table in the same database transaction as the business data, then relayed to the message broker afterward.
The problem it solves
Updating the database and publishing a message are two separate systems, so they cannot share one transaction. This is the dual-write problem:
- Commit the row, then crash before publishing. The event is lost.
- Publish first, then the transaction rolls back. Consumers react to something that never happened.
Wrapping both in a distributed transaction (two-phase commit) is slow, fragile, and often unsupported by modern brokers.
How it works
- In one local transaction, write the business change and insert a row into an
outboxtable. - The transaction commits atomically. Both succeed or neither does.
- A separate relay reads unpublished outbox rows and sends them to the broker.
- On a confirmed publish, mark the row as sent (or delete it).
Because the relay runs independently, a crash only delays delivery; nothing is lost.
BEGIN;
INSERT INTO orders (id, customer_id, status)
VALUES ('o-123', 'c-9', 'placed');
INSERT INTO outbox (id, aggregate, type, payload, created_at)
VALUES (
gen_random_uuid(),
'order',
'OrderPlaced',
'{"orderId":"o-123","customerId":"c-9"}',
now()
);
COMMIT;Publishing the outbox
Two common ways to move rows from the table to the broker:
- Polling publisher, a background worker queries unsent rows on an interval and publishes them. Simple, easy to operate.
- Change data capture (CDC), tools like Debezium tail the database transaction log and stream outbox inserts to the broker with low latency and no polling load.
Delivery guarantees
The outbox gives at-least-once delivery: a relay may publish a row, then crash before recording success, and republish on restart. Consumers must therefore be idempotent, deduping on the event ID or using idempotency keys so reprocessing is safe.
Trade-offs
- Adds an outbox table, a relay process, and cleanup of published rows.
- Introduces latency between commit and publish (polling interval or CDC lag).
- Ordering across aggregates is not guaranteed; preserve per-aggregate order when it matters.
- Pairs naturally with the inbox pattern on the consumer side for end-to-end exactly-once effects.
Related reading
See event-driven architecture for the messaging model the outbox feeds, and webhook reliability patterns for the same delivery concerns over HTTP.
