Transactional outbox delivery

The database committed. Did the event leave?

A business write and a message publish can fail independently. Save the intention to publish in the same transaction as the business change, then deliver it through a recoverable worker.

In this article

The gap between two successful operations

An order service commits a new order and then publishes an OrderCreated event. The process stops after the database commit but before the publish. The customer can see the order, while the fulfilment system never learns that it exists.

Reversing the order of operations creates a different problem. Publishing first can tell fulfilment about an order whose database transaction later fails. Two sequential calls do not become atomic simply because they are close together in the code.

A transactional outbox addresses this gap by storing the business change and an event record in one local transaction. A separate publisher reads committed event records and sends them to the broker. If the publisher stops, the undelivered intention remains available for recovery.

Make the event record durable and specific

Give each event a stable identifier, type, schema version, aggregate identifier and payload. The aggregate is the business entity whose changes the event describes, such as one order. Include a sequence or version where consumers need to reason about change order.

Keep the payload focused on the event contract. Serialising an entire database entity can expose unrelated fields and couple consumers to internal storage changes.

Commit first, publish recoverablyThe order and event intention share a database transaction. Delivery and consumer processing have their own recovery rules.
  1. Business transactionCommit the order and outbox event together
  2. PublisherClaim committed events and send stable identifiers
  3. BrokerDeliver under its documented retry and ordering semantics
  4. ConsumerApply the effect with duplicate protection

Expect a duplicate window

The publisher can send an event successfully and stop before marking it delivered. On restart, it may send the same event again. The outbox removes the lost-intention gap, but it does not automatically make every downstream effect happen exactly once.

Consumers need a duplicate strategy appropriate to their effect. For a local database update, a processed-event record can be committed with the effect in one transaction. For an external call, use the downstream idempotency or reconciliation contract.

Broker deduplication can help within its documented scope, but it should not be confused with proof that an arbitrary external side effect cannot repeat. Review the whole path.

Decide what ordering the business needs

Some consumers can apply independent events in any order. Others need changes for one order to be processed sequentially. State that requirement and enforce it through publisher partitioning, broker capabilities or consumer version checks.

Sorting a query by event time does not guarantee completion order when several publishers work concurrently. A delayed earlier event can be overtaken. Design for the required per-entity order rather than assuming one global timestamp solves it.

Operate the backlog as business work

Monitor the oldest unpublished event and unresolved consumer outcomes, not only queue depth. A single blocked cancellation can matter more than many routine notifications.

Test a crash before publish and another after broker acceptance. Confirm no committed business event is silently lost and duplicate delivery does not repeat the business effect. The pattern is useful when its remaining delivery and recovery responsibilities are explicit.

Primary sources

AWS: transactional outbox patternAWS: SQS at-least-once delivery

References checked 11 September 2026.