2.5 KiB
Domain Events
Use events for a real notification or asynchronous coordination need. Crossing an aggregate boundary alone is not a reason to introduce an event bus.
Define the fact and consumers
Name a domain event as a completed business fact, distinct from a command or pending operation. Identify its actual consumers and the minimum non-sensitive data they need. Avoid serializing the entire aggregate or inventing metadata fields without a purpose.
Distinguish internal events from externally consumed contracts. Translate when semantics differ; do not introduce a separate layer merely to change the name.
Persistence and delivery
An event must not claim a state change that rolled back. If delivery after commit must be durable, use the project's existing transactional publication mechanism or justify an outbox that records the state change and pending event atomically.
For retried delivery, make handlers idempotent using an appropriate key, conditional write, or naturally idempotent operation. Coordinate deduplication and the effect atomically where required; recording a key before a failed effect can lose work.
Define ordering requirements, retry limits, and observable failure handling. Test duplicate, delayed, out-of-order, and failing delivery where those modes affect the invariant. Do not claim exactly-once behavior merely because duplicate messages are filtered.
Do not weaken atomic business operations
Preserve the cross-aggregate transactions accepted in ADR-0011. Asynchronous coordination is suitable only when partial progress and delay are valid. A saga or process manager is justified only by an actual long-running workflow, with explicit recovery semantics.
Event sourcing is a separate choice
Event sourcing makes stored events the source of truth; publishing an event does not require it. Consider it only for a demonstrated need for historical reconstruction or temporal queries that the existing model cannot reasonably serve.
If chosen, define replay behavior without repeating external side effects, concurrency protection, and the actual retention obligations. Snapshots are an optimization, not initial scaffolding.
Follow the development lifecycle: replace and reseed disposable development events. Retained-history upcasters or compatibility versions require an explicit later production-data decision; genuine external event contracts remain external constraints.