Message flow
One committed row becomes one Kafka message. Below, three aggregates make that trip at once — each keeps its own order, at its own pace, and a failure on one never touches the others.
What the picture is hiding
BEGIN TX UPDATE aggregate SET version = version + 1 WHERE id = ? INSERT INTO tandem_outbox (aggregate_id, bucket, seq, payload) COMMIT TX
The domain change and the outbox row are one commit, as above. Nothing publishes yet (stage 1 is just a row becoming durable).
bucket = hash(aggregate_id) mod B, computed once at insert. Every event of
an aggregate lands in the same bucket, and each bucket is owned by exactly one worker
(ordering is structural, not a lock that a race can lose).
A worker claims its bucket's PENDING rows in id order with
FOR UPDATE SKIP LOCKED and marks them IN_FLIGHT under a lease.
If the worker dies mid-publish, the lease expires and another worker reclaims the row.
The loop claims back to back while work remains, and waits only when it finds nothing. That wait is short right after a claim that found rows and grows while the bucket stays quiet, so a busy bucket is checked often and an idle one is left alone.
The row is wrapped in a CloudEvents envelope and published with
key = aggregate_id. Only a Kafka ack marks the row DONE (a
crash between the two produces a duplicate, never a gap).
After its retries are exhausted, a row is marked FAILED and the relay stops
advancing that aggregate (its later events queue behind it). Every other
aggregate, including ones sharing its bucket, keeps flowing: SKIP LOCKED
steps around the stuck row instead of stalling on it.