A service that updates its database and then publishes an event performs two writes with no transaction spanning them — the dual-write problem. A crash between the writes either loses the event or emits a ghost event for a change that rolled back. The transactional outbox removes the broker from the critical write path: the service stores the event in the same database and transaction; a relay forwards it afterwards.

Transactional outbox: the order service writes its state change and an OrderPaid event record to its own database in one ACID transaction; a message relay polls the outbox or tails the commit log, publishes to the message broker, and marks rows sent after acknowledgement; consumers deduplicate by event ID.

How It Works

  • The business transaction writes its state change plus one outbox row (event ID, aggregate ID, type, payload) and commits as one ACID unit.
  • A relay picks up unsent rows by polling the outbox table or tailing the database commit log via change data capture.
  • The relay publishes each row, preserves per-aggregate order via a partition key, and marks rows sent only after the broker acknowledges.
  • Delivery is at least once: a relay crash between publish and mark-sent republishes the row, so consumers deduplicate by event ID.

Failure Modes

  • A stalled relay lets unsent rows pile up: state changes commit, but downstream reaction waits until the relay catches up.
  • Redelivered events double-trigger consumers that skip deduplication — a payment notification sent twice.
  • Parallel relay instances without a partition key reorder events for the same aggregate.

Verification

  • Outbox lag (age of the oldest unsent row) stays below the delivery objective; alert past the threshold.
  • Failure injection kills the service between commit and publish: recovery yields zero lost and zero ghost events across 1,000 runs.
  • Replaying a day’s events against consumers yields an identical end state, proving deduplication works.
  • Polling publisher vs. log tailing — polling adds query load and latency; change data capture cuts both but couples the relay to the database’s replication protocol.
  • Event Sourcing — makes the event log the primary store, dissolving the dual write entirely at the cost of a new persistence model.
  • Saga Pattern — each saga step publishes its event through an outbox, so a lost publish cannot stall the saga.

Example

An order service marks order 4711 as paid and, in the same local transaction, writes an OrderPaid record to its outbox. The commit makes both durable at once. Moments later the relay picks up the row and publishes it; invoicing and shipping react. Had the service crashed right after the commit, the event would wait in the outbox and go out on restart — never lost, never a ghost.

References