ADR 0006: Events — transactional outbox, inbox deduplication, versioned schemas
Status: Accepted
Date: 2026-07-19
Context
Integration events must survive crashes, never duplicate business effects, and evolve safely.
Decision
- Domain events stay inside the module transaction (in-memory dispatch).
- Integration events are written to the
outboxtable in the same transaction as the state change, then published asynchronously by the relay to the queue.
- Consumers record
inboxdeduplication entries (event id + consumer) and are idempotent by contract.
- Event envelopes carry
id,type(module.entity.verb, semver'd),tenantId,actor,occurredAt,correlationId,causationId,version, and a payload of IDs/references — never secrets or large mutable snapshots.
- Failed deliveries land in a dead-letter workflow with replay (
apps/control→ Jobs).
- Schema changes to published events require compatibility tests (
pnpm test --filter */contract).
Consequences
- Exactly-once *business effect* with at-least-once delivery; duplicates are provably harmless (reference journey 8).
Durable worker update (2026-10-11)
The PostgreSQL relay dispatches directly to registered worker handlers; a separate queue is optional. Claims carry renewable leases and fencing tokens, and future availableAt timestamps implement durable schedules. Identity email receipts use event/consumer keys; scheduled messages and reminders use tenant-scoped delivery keys. Recurring maintenance and reconciliation use deterministic time-bucket/cursor/tenant event IDs, bounded directory pages, and independent tenant jobs.
External SMTP acceptance cannot commit atomically with a database receipt. A crash after acceptance can cause another transport attempt, so stable Message-ID headers complement transactional deduplication. Exactly-once mailbox delivery is not guaranteed. See worker delivery for the implemented jobs, encrypted deferred payloads, and authenticated feedback contract.