Tandem

Message flow

How a message moves through Tandem

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

One commit, no dual write

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. Nothing publishes yet (stage 1 is just a row becoming durable).

Bucketed, not locked

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).

SKIP LOCKED, then a lease

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.

CloudEvents, keyed by aggregate

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).

One poison row, one blocked aggregate

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.