The hard part is not emitting an event but coordinating it with the order
When a shopper completes checkout, the order service must change its database and tell inventory, billing, email, analytics or fraud systems what happened. The obvious implementation performs two independent writes: save the order, then publish a message. That gap creates the dual-write failure window. If the database commits and the broker does not receive the event, the order exists but downstream work never starts. If the message leaves first and persistence fails, consumers act on an order that does not exist.
A distributed transaction could coordinate multiple resources, but it is often unavailable, undesirable or unsupported across the stack. The transactional outbox narrows the atomic boundary to one database. In the same local transaction that creates or updates an order, the application inserts an outbox row describing the event. A separate relay forwards it later. The business state and the intent to publish therefore become visible together or remain invisible together.
Treat the outbox as an operational contract
An outbox table is not an unstructured temporary folder. Each row should carry a stable event identifier, event type, aggregate identifier, version or sequence, creation time and the minimum payload consumers need. The aggregate may be an order, payment or inventory reservation. A key such as order_id makes per-entity ordering possible when the transport preserves partition order.
The payload should not copy every database column. An event describes a domain fact that has already happened, through a versionable contract without payment tokens, secrets or unnecessary personal data. Technical headers may carry correlation and tracing context, but the schema must remain intelligible to consumers. Define ownership, compatibility rules, data classification and evolution before connecting the first subscriber.
One local transaction for state and event
The outbox insert must participate in the same commit as the business change. Saving the order, committing it and then opening another database transaction for the event recreates the failure window. Frameworks that collect domain events in memory must flush them into the outbox inside the current transaction and fail the whole commit when the outbox cannot be written.
Choose a polling relay or change data capture
A polling publisher regularly selects unpublished rows, sends messages and records progress. It is explicit, portable and easy to reason about, but concurrency and locking require care. In PostgreSQL, multiple workers can select deterministic batches and use FOR UPDATE SKIP LOCKED so they avoid rows held by another worker. This supports queue-like access to a table, but it does not create fairness and does not replace retry policy.
Change data capture reads committed changes from the database log. A connector can observe inserts into the outbox and transform them into events routed to suitable topics. Debezium documents an Outbox Event Router that uses fields such as aggregate identifier, event type and payload to assemble outgoing messages. CDC removes application polling, but adds connectors, offsets, permissions and recovery procedures that need explicit ownership.
Select the model the team can operate
Polling often fits moderate volumes and teams that want a small relay inside their runtime. CDC fits organisations that already operate a streaming platform and manage the database log as production infrastructure. Both models need measurements for commit-to-publish delay, backlog, failures and recovery capacity. The technology alone does not guarantee correctness; the local transaction and verified invariants do.
Assume at-least-once delivery and design idempotency
The relay can stop after sending a message but before recording that its outbox row was processed. When it restarts, it sends the event again. Duplicates are therefore normal in an at-least-once pipeline. The outbox does not justify an end-to-end exactly-once claim: relay, broker, consumer and external systems each have separate acknowledgement boundaries.
Every consumer should recognise the event identifier. A local inbox or deduplication table can store it in the same transaction that applies the business effect. If the identifier already exists, the consumer acknowledges without repeating the change. External side effects, such as a payment-provider request, also need recipient-supported idempotency keys and independent reconciliation. Deduplication records must live at least as long as the realistic retry and replay window.
Preserve order per aggregate rather than globally
Most commerce flows need OrderCreated before OrderCancelled for the same order; they do not need every store event in one serial stream. A monotonic aggregate sequence, consistent partition key and consumer that rejects or quarantines out-of-order versions make the rule testable. Global ordering sacrifices parallelism and rarely matches the real business invariant.
Govern retries, retention and quarantine
The relay must separate transient transport errors from records that cannot be published. Transient failures follow limited retries with backoff and jitter. Poison records move to quarantine with a reason, protected payload and repair procedure. Retrying malformed data forever can block a batch or consume capacity; a separate lane keeps healthy events moving without erasing evidence.
Retention balances auditability, replay and storage cost. Deleting published rows immediately makes incidents difficult to reconstruct, while keeping all rows forever burdens indexes and queries. Archive or partition according to an explicit policy and preserve the identifiers needed for deduplication. Replay must be authorised, bounded by range and observable because it can repeat business effects even when transport works exactly as designed.
Observe the distance between commit and consequence
Core signals include the age of the oldest unpublished row, backlog depth, commit-to-send latency, publishing rate, retries, quarantine count and duplicates detected by consumers. Segment them by event type and critical aggregate. A shallow queue can still hide one stuck order, so distributions and maximum age matter alongside totals.
Logs and traces should connect event_id, aggregate_id, commit, relay attempt and consumption without exposing sensitive payloads. Alerts should name the business effect: confirmed orders without stock reservation, delayed invoices or unsent customer messages. Dashboards and runbooks must identify who can pause the relay, replay an event and release quarantined data safely.
Crash-test every awkward boundary
The decisive tests stop processes before commit, after commit but before polling, after publish but before acknowledgement, and while a consumer applies its effect. After each restart, verify that no event is lost, duplicate delivery is harmless and aggregate ordering remains valid. Also simulate broker downtime, backlog growth, malformed rows and temporary loss of a CDC connector.
Rollout can begin with shadow observation, limited event types and reconciliation between orders and downstream effects. A scheduled control should find orders without outbox rows, stale outbox entries and missing consequences. To make this pattern operational across checkout, inventory and automation, review our ecommerce systems services or discuss the architecture with our team. The goal is not to promise that duplicates never happen, but to make every state recoverable and every discrepancy visible.
Frequently asked questions
Why is a dual write between a database and broker unsafe?
The two systems can acknowledge at different times, so a crash can leave a committed order without an event or an event for a change that never committed. The outbox places both writes in one local commit.
Does a transactional outbox guarantee exactly-once delivery?
No. A relay may publish again after a crash and acknowledgements span several systems. The design assumes at-least-once delivery and uses event identifiers, inboxes or deduplication to make consumers idempotent.
Should an outbox use polling or change data capture?
Choose the model the team can operate. Polling is explicit and portable; CDC reads the database log and requires connector and offset management. Both need backlog, delay and recovery monitoring.
How should ecommerce events preserve order?
Use a sequence per aggregate, a consistent partition key and a consumer that validates versions. Usually events must be ordered for one order or payment, not globally across the store.
Related articles
Ecommerce async queues: backpressure, DLQs and safe replay
Separate order confirmation from asynchronous effects and govern workers, retries, dead-letter queues and replay without duplicating actions or hiding failures.
Reliable ecommerce webhooks: idempotency, retries and observability
An implementation blueprint for receiving order and payment events, preventing duplicate effects, recovering failures and measuring the entire pipeline.
Prevent ecommerce overselling with inventory reservations and concurrency control
An implementation model for separating physical, available and reserved stock, allocating the last unit atomically, and reconciling errors across sales channels.
