Skip to content

Transactional Outbox and Eventual Consistency

The transactional outbox addresses a dual-write problem: an application must change its database and publish a message, but there is no single atomic commit across both systems. It writes the business change and an outbox record in one local database transaction. A separate relay later publishes the record.

flowchart LR
    A["Application transaction"] --> B["Business rows"]
    A --> C["Outbox row"]
    C --> D["Relay"]
    D --> E["Message broker"]
    E --> F["Idempotent consumer"]

Delivery protocol

  1. Validate the command and begin a local transaction.
  2. Write domain state and an event with a stable identifier.
  3. Commit both or neither.
  4. A polling publisher or log-based relay claims unpublished records.
  5. Publish, then mark or checkpoint progress.

A crash after publish but before marking causes redelivery. Therefore the normal claim is at-least-once publication, not exactly once. Consumers need idempotency or deduplication.

Ordering and schema

Global ordering is expensive and often unnecessary. State whether order is required per aggregate, partition key, or event stream, and include an aggregate version when consumers must detect gaps. Events are durable contracts: include type and schema version, avoid exposing internal entities, and evolve consumers compatibly.

Relay operations

The relay needs bounded batches, locking or claiming semantics, retries with a deadline, poison-record handling, monitoring, and retention cleanup. Multiple relay instances must not turn claims into data loss. Backlog age is usually more meaningful than row count alone.

Spring application events and transaction-bound listeners can coordinate in-process behavior but are not durable publication. A process crash after the database commit can still lose an in-memory notification; use an outbox when durability across that boundary is required.

Eventual consistency

Consumers observe the change after a delay. Product behavior must account for read-your-writes expectations, duplicate or late events, reconciliation, and temporarily divergent views. A saga coordinates a multi-step workflow; it does not make several local transactions globally atomic.

Test rollback, crash before and after publish, relay concurrency, duplicates, out-of-order delivery, poison records, schema evolution, cleanup, and recovery from a growing backlog.

See Spring's official transaction-bound events.