The problem: a service needs to save an order in the database and publish the OrderCreated event to Kafka. They are two systems, with no shared transaction:
- It saves and crashes before publishing: the order exists, but nobody finds out.
- It publishes and the database commit fails: other services react to an order that doesn’t exist.
The solution: write the event to an outbox table, in the same database and in the same transaction as the order. A separate process reads that table and publishes to the broker.
BEGIN;INSERT INTO orders (id, status) VALUES (42, 'CREATED');INSERT INTO outbox (aggregate_id, type, payload) VALUES (42, 'OrderCreated', '{"id":42}');COMMIT;Either both rows exist, or neither does. The relay that publishes can be:
- Polling: a job that periodically reads unpublished records and marks the ones sent.
- CDC: Debezium reads the database transaction log and publishes every insert into the outbox, without extra queries.
Since the relay can crash between publishing and marking the record, the event can go out twice: the guarantee is “at least once”, and consumers need to be idempotent.
- To dig deeper: Transactional outbox (microservices.io)