Marcelo Pastorino, Software Developer

Outbox Pattern

Reliably publishing events by committing them in the same transaction as state changes.

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

  1. In one local transaction, write the business change and insert a row into an outbox table.
  2. The transaction commits atomically. Both succeed or neither does.
  3. A separate relay reads unpublished outbox rows and sends them to the broker.
  4. 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.

See event-driven architecture for the messaging model the outbox feeds, and webhook reliability patterns for the same delivery concerns over HTTP.

See also

  • Event-Driven Architecture, Decoupling producers and consumers through asynchronous messaging.
  • Saga Pattern, Managing distributed transactions across services with a sequence of local transactions and compensating actions.
  • Inbox Pattern, Deduplicating inbound messages so consumer side effects run exactly once.
  • Eventual Consistency, A consistency model where replicas converge to the same state over time, allowing temporary divergence after a write.
  • Webhook Reliability Patterns, Designing HTTP callbacks that survive retries, duplicates, and outages.
  • Webhook, An HTTP callback that notifies another system when an event occurs.
  • Idempotency, Safe to run more than once without changing the outcome beyond the first successful apply.
  • Message Broker, Middleware that receives messages from producers and routes them to consumers, decoupling the two.
  • Two-Phase Commit, A protocol that coordinates a single atomic transaction across multiple participants using a prepare phase and a commit phase.

On Wikipedia