7. Idempotent Consumers Remember Processed Events
Why consumers of the core services detect duplicates by storing what they have processed, instead of relying on idempotent business logic.
- Status: accepted
- Date: 2025-01-30
- Deciders: Jannik Alexander, Stefan Bente, Daniel Köllgen (governance board)
Context
Events in the MSD are delivered at least once: the outbox may publish an event twice (ADR-0008), and Kafka redelivers a batch when processing is interrupted. Players may also send the same intent twice. Processing a duplicate must not change the outcome — a robot must not move two fields for one move intent.
The solution must be correct for relative state changes (moving a robot, deducting credits), work the same way in every core service, and avoid expensive broker transactions. The options are described in Idempotent Event Consumer.
Decision
- Core services MUST detect duplicates by storing what they have processed, in the same database transaction as the domain changes the event causes.
- Duplicates are identified by one of the standard headers
(ADR-0005):
event_id— store the ids of processed events and look them up before processing; orsequence_id— store the highest sequence id processed per topic and partition (a “high watermark”) and skip anything at or below it. Cheaper, but only for events from the core services.
- Duplicate detection for player intents MUST use
event_id, as asequence_idset by players cannot be trusted. - Services MUST take particular care with transaction boundaries (e.g.
@Transactional), so that the stored marker and the domain change commit or roll back together.
Alternatives
- Idempotent business logic — rejected because relative state changes such as moving robots cannot easily be made idempotent without remodelling them (e.g. as event-sourced transactions).
- Transactional event processing (Kafka transactions) — rejected because transactions are costly, add latency and do not include the service’s database.
Consequences
- Good: works for any business logic, including relative changes.
- Good: the check can move into a shared library (the consumer-lib) once it is proven.
- Bad: every consumer needs an additional table and a lookup per event.
- Bad: relies on one consumer per partition; without it, the lookup needs locking.
Links
- Related to ADR-0008
- Idempotent Event Consumer
Last modified October 22, 2026: Update documentation (f481966)