Event-driven architecture decouples producers from consumers in time, but introduces duplicate delivery, ordering limits, schema evolution, replay, and operational complexity. A transactional outbox commits domain state and an event together; publishers deliver at least once; consumers use an inbox or unique effect key to make processing idempotent.
Inside one PostgreSQL transaction:
A publisher claims unpublished rows, sends them, and marks progress. A crash can cause duplicate publication, so consumers must deduplicate by event/effect ID. Do not delete outbox rows without a retention and reconciliation strategy.
Use a stable envelope: event ID, type, schema version, occurred time, tenant, aggregate ID/version, correlation/causation IDs, safe payload, and trace context. Minimize personal data and do not include secrets. Events state something that occurred; commands request an action and have one intended owner.
Prefer additive optional fields, tolerant consumers, and versioned semantic changes. Test producers against consumer contracts, keep a schema catalog, and define replay behavior. Consumers must not trigger duplicate emails, model calls, or tool effects during replay; use effect identities and a replay mode.
A saga persists a multi-step business process across services and failures. Each step has an idempotent command, outcome, timeout, and possible compensation. Compensation is not rollback: an email cannot be unsent, and a refund reversal has business meaning and may fail.
Events can trigger classification, indexing, evaluation, and notifications without blocking ticket writes. They must not blindly trigger high-cost model calls for every replay or let untrusted event text become tool authority. Include feature and budget policy at the consumer.
Design knowledge_document.published triggering indexing, evaluation, and notification. Simulate duplicate delivery, out-of-order deletion, replay, and one failed consumer.
Acceptance criteria: publication and event cannot diverge, every consumer is independently idempotent, deletion wins according to aggregate version, replay suppresses unwanted external effects, and failure is observable/recoverable.