An order is confirmed in PostgreSQL, but the downstream service responsible for the next step never receives the event. The API responded with success, the record exists in the database, yet the workflow stalled without a single transaction error.
This failure originates from a deceptively simple code pattern: executing COMMIT followed immediately by publish() to the message broker. These two operations are not atomic. A process crash or network partition between them leaves the database mutation committed while the event vanishes.
Inverting the call order does not solve the problem: publishing before the commit risks broadcasting a state change that the database subsequently rolls back, causing downstream consumers to react to phantom events that were never durably persisted.
The Transactional Outbox pattern resolves this dual-write dilemma by persisting both the domain mutation and an outbox event record within the exact same database transaction. When the transaction commits, the intent to publish is guaranteed to be durably stored alongside the business data.
A separate background relay process polls pending events or tails the database write-ahead log via Change Data Capture (CDC) to publish them to the broker. If the broker is temporarily unavailable, the backlog grows, but events remain safely persisted until delivery resumes.
However, a second failure window still exists: the broker acknowledges receipt, but the relay crashes before updating the outbox record status. Upon restart, the relay re-publishes the same event. Consequently, downstream consumers must track a stable event_id and handle duplicate deliveries idempotently.
This design delivers an at-least-once rather than exactly-once delivery guarantee. Publication latency, per-aggregate ordering, reprocessing semantics, and outbox table retention require explicit architectural trade-offs, metrics, and backlog alerting.
The core principle is not pretending databases and brokers never crash, but ensuring the intent to publish cannot vanish while the business change persists. An event can only safely cross an asynchronous system boundary when its origin is durable.