8. At-Least-Once Publication via the Transactional Outbox
- Status: accepted
- Date: 2025-07-03
- Deciders: Jannik Alexander, Stefan Bente, Daniel Köllgen (governance board)
Context
A core service changes its database and publishes events about the change to Kafka — two independent systems. If the service crashes between the two writes, database and published events disagree: an event is lost, or an event is published for a change that was rolled back. Players and other services build their state from these events (ADR-0006), so events must not be lost, and events for the same partition must arrive in order.
On 2025-01-30 the board judged this of little practical relevance for the MSD and decided to ignore it for the first version. It was implemented nevertheless as the outbox-lib and outbox-service, presented to the board on 2025-07-03. This ADR records that decision and replaces the earlier one. The options are described in Atomic Event Publication.
Decision
- Core services MUST publish their events through the outbox-lib: events are written to an outbox table in the same local transaction as the domain changes.
- Each core service MUST be paired with its own outbox-service sidecar, which polls the outbox per configured topic, publishes the events to Kafka in order per partition, and deletes them afterwards.
- The delivery guarantee is at-least-once, in-order per partition. If the outbox-service crashes after publishing but before deleting, events are published again. Consumers MUST therefore be idempotent (ADR-0007).
- The outbox-lib assigns the partition and a strictly increasing
sequence_idper partition when the event is written, so consumers can detect duplicates. - A partition MUST be read by exactly one outbox-service instance. There is no leader election between instances; more than one reader causes race conditions.
- Players are not provided with an outbox. They MAY implement one.
Alternatives
- Ignore the problem (publish directly after committing) — the original decision of 2025-01-30; rejected because crashes between commit and publish lose events or publish events for rolled-back changes.
- Global transactions with a transaction manager — rejected because, although it gives exactly-once delivery, it is complex, often requires commercial products, and reduces response time and throughput of both systems.
Consequences
- Good: no event is lost, and no event is published for a rolled-back change.
- Good: order per partition is preserved end to end.
- Good: the pattern is shared as a library and a service, so core services only write to the outbox.
- Bad: duplicates are possible, so every consumer needs duplicate detection.
- Bad: an additional service per core service to deploy and operate; publishing is delayed by the polling interval.
Links
- Related to ADR-0007
- Atomic Event Publication