Outbox + saga = reliable distributed workflows
The dual-write problem
Your service must do two things atomically: update its database and publish an
event. There is no distributed transaction across Postgres and Kafka, so a naive
db.commit(); kafka.send() can crash between the two — DB updated, event lost (or
vice-versa). Silent, corrupting drift.
The Transactional Outbox fixes it: within the same DB transaction that changes
business state, insert a row into an outbox table. A separate relay (Debezium CDC on
the outbox table, or a poller) publishes those rows to Kafka and marks them sent.
Because the state change and the outbox row commit together, you can never have one
without the other — the event is guaranteed to follow the state.
Sagas — distributed transactions without 2PC
A saga is a sequence of local transactions, each emitting an event that triggers
the next. There's no global rollback; instead each step has a compensating action.
If PaymentCaptured succeeds but ShipmentBooked fails, the saga emits
RefundPayment to compensate. Sagas trade ACID atomicity for eventual consistency
plus explicit compensation — the only realistic model across service boundaries.