Idempotent consumers: the half the outbox exports
Who pays for at-least-once delivery when a transaction spans systems?
The half the outbox exports
The outbox and every saga step guarantee at-least-once delivery, so the consumer must make a repeat harmless. How that is done (exactly-once is a property of effects, never of delivery; natural idempotence or a dedup key; the retention window a dedup store cannot verify) is fault tolerance's subject. For a distributed transaction the point is where the cost lands: the consumer that the outbox bills. A minimal consumer that dedups on the event's ID, inside the same local transaction as its effect:
def handle(event):
with db.transaction():
inserted = db.execute(
"INSERT INTO processed_events (event_id) VALUES (%s) ON CONFLICT DO NOTHING",
[event.id]).rowcount
if inserted == 0:
return # duplicate: already handled
send_eticket(event.order_id) # NOTE: if this call is itself an external
# effect, it needs its own idempotency key