The order committed, but nobody heard about it
A hypothetical order transaction commits. Before the application publishes OrderCreated, the process dies. The customer owns an order that fulfillment will never hear about.
You reverse the calls. Now a publish succeeds and the database transaction fails. Fulfillment sees an order that never existed.
Make the announcement part of the local fact
What can the database commit atomically without asking the broker to join its transaction?
sql-- Schema sketch: outbox.event_id is unique. BEGIN; UPDATE orders SET state = 'confirmed', version = version + 1 WHERE order_id = :order AND version = :expected RETURNING version; -- Abort if the expected version did not match. INSERT INTO outbox(event_id, aggregate_id, aggregate_version, payload) VALUES (:event, :order, :new_version, :payload); COMMIT;
The relay waits for durable broker acknowledgement before marking/removing the outbox entry. A crash after publishing but before that mark causes redelivery. That is why the consumer's atomic inbox/effect boundary is a prerequisite, not an optional enhancement.
The cursor skips a committed event
Transaction A allocates sequence 41 and pauses. Transaction B allocates 42 and commits. A poller reads 42 and stores âlast seen = 42.â Then A commits 41.
Will WHERE sequence > last_seen find A? What property did the cursor assume?
Multiple pollers using row claims can publish concurrently, but preserving per-aggregate order requires more than SKIP LOCKED on individual events. Own an aggregate/bucket, serialize versions, and keep the partition key stable. A downstream consumer can skip known duplicates, apply exactly the next version, and park a gap; skipping every older version is appropriate only when event semantics permit it.
CDC trades polling work for log obligations
Log-based change data capture reads committed changes from an engine's replication stream. It can publish outbox events or raw row changes. Domain events express business meaning; raw CDC exposes storage-schema coupling. Neither automatically guarantees exactly-once downstream effects.
The CDC reader has a position and a retention horizon. A stalled replication slot may retain WAL until storage fills; a retention cap may instead invalidate the reader's position. At 10 MB/s, a two-hour stall needs roughly 72 GB of retained log. Alarm on bytes and time to exhaustion, including a quiet-source heartbeat strategy where the connector requires it.
Bootstrap must connect a snapshot to the stream without gaps. One pattern captures a stream position before a compatible snapshot, then applies changes with version-conditional writes so duplicates from the overlap are harmless. The exact protocol depends on snapshot isolation and connector behavior; do not invent a universal âexport then start at latestâ handoff.
If retention is lost, rebuild from a consistent snapshot or durable outbox archive. If a partition splits, process lineage in a way that preserves each entity's ordering; if a Kafka partition count changes, old and new events can be routed differently. Routing is part of the ordering chain.
Deletes and schema changes are events, too
Version deletion markers so old updates cannot resurrect deleted entities. Keep tombstones through the accepted replay horizon or require a generation reset/rebuild. Schema evolution should allow old consumers to keep working where possible; incompatible semantics deserve a new version/type and a migration plan.
Poison events require bounded retries, durable failure capture, and per-key ordering decisions. Advancing the global cursor while forgetting the bad event can create an irreversible hole. Applying a full snapshot later may repair a current view but cannot necessarily recreate every historical business side effect.
Mental model: the outbox makes intent atomic with the change. The relay, ordering protocol, and consumer determine how that intent becomes an effect.
Transfer: two projections
A relay updates search successfully and times out updating the cache. Can it mark the event delivered because one projection succeeded?
Source trail: 12-change-data-capture-and-outbox-pattern, 05-change-streams-and-transactional-outbox. See the source map for consolidation and reference-retained material.