← Concepts

阅读中文版本

Outbox Pattern

The transactional outbox pattern writes a business change and its event in one database transaction, then relays the event so other services see it exactly when the change commits.

data-consistency

Definition

The outbox pattern stores outgoing events in an outbox table in the same database transaction that changes business data. A separate relay process reads unpublished rows and sends them to a broker or another service, marking them as published. Because the business write and the event write commit atomically, the system avoids the dual-write problem where a database commit succeeds but publishing fails, or publishing succeeds but the transaction rolls back.

Context and scope

Outboxes are used when a service must reliably notify other services after a local state change, especially in microservice architectures, event-driven systems, and workflows that cannot afford lost or phantom events.

Common misconceptions

  • The outbox guarantees exactly-once delivery — it guarantees at-least-once; consumers must deduplicate
  • The relay can publish before the transaction commits — that reintroduces the dual-write failure
  • The outbox table never needs cleanup — published rows must be archived or deleted or it grows without bound

Anti-patterns

  • Writing the database row, committing, and then calling the broker in application code with no retry record
  • Publishing inside the transaction and assuming the broker call is atomic with the database
  • Consumers that are not idempotent, so a redelivered event applies twice

Related concepts

Related examples

Authoritative source

https://microservices.io/patterns/data/transactional-outbox.html

The dual-write problem

A service creates an order and must publish an OrderCreated event. If it commits the order and then publishes, a crash between the two steps loses the event. If it publishes first and then the database commit fails, consumers see an order that never existed. Retrying the publish or the commit independently does not solve the problem, because the two systems do not share a transaction.

An outbox moves the event into the same database as the business data. The transaction inserts the order and the event row together. Either both exist or neither does. A relay publishes the event afterward, retrying until it succeeds. The gap between commit and publish becomes a delay, not a lost event.

CREATE TABLE outbox (
  id           bigserial PRIMARY KEY,
  aggregate_id text NOT NULL,
  event_type   text NOT NULL,
  payload      jsonb NOT NULL,
  created_at   timestamptz NOT NULL DEFAULT now(),
  published_at timestamptz
);

BEGIN;
INSERT INTO orders (id, customer_id, total)
VALUES ('order-1', 'customer-9', 4200);
INSERT INTO outbox (aggregate_id, event_type, payload)
VALUES ('order-1', 'OrderCreated', '{"orderId":"order-1","total":4200}');
COMMIT;

The business write and the event insert are in one transaction. Keep the outbox insert in the same repository or unit of work as the business change; a service method that sometimes remembers the outbox and sometimes does not is a bug waiting to happen.

Relay and consume

A relay claims unpublished rows, publishes them, and marks them published. Polling is simple but adds latency and database load. Change data capture reads the database log and scales better, at the cost of operational complexity. Whichever mechanism you use, the relay must be idempotent: publishing the same row twice is allowed, losing it is not.

Consumers must deduplicate by event id or by a business key. Store processed ids for a retention window at least as long as the broker can redeliver. Ordering is per aggregate, not global; do not assume that two events for different orders arrive in any particular order. If ordering matters within an aggregate, include a sequence number and reject or buffer out-of-order events.

Operational concerns

Monitor the age of the oldest unpublished row, publish failures, and consumer lag. A healthy outbox has a small, bounded backlog. Add an alert when the backlog exceeds a threshold or when a row remains unpublished longer than the delivery SLO. Clean up published rows with a retention policy, and archive payloads if audit requirements demand it.

The outbox payload should contain enough information for consumers to act without calling back into the producing service, but it should not carry secrets or unbounded documents. Version the event schema and evolve it compatibly. An event is a public contract; changing it without versioning breaks consumers you may not control.

When to use it

Use an outbox when a local database change must reliably trigger an external event and losing or inventing that event is unacceptable. The advantage is atomicity without a distributed transaction; the drawback is additional storage, a relay to operate, and at-least-once delivery that consumers must deduplicate. Compared to publishing directly from application code, an outbox adds latency and infrastructure but removes the dual-write failure mode. If occasional lost events are acceptable and the event is purely advisory, a direct publish with retries may be simpler.