Outbox Pattern
Updated June 3, 2026The Dual-Write Problem
In a modern event-driven architecture, a microservice often needs to do two things when an action occurs:
- Update its own database (e.g.,
UPDATE users SET status='active'). - Publish an event to a message broker like Kafka (e.g.,
UserActivatedEvent) so other services can react.
This seems simple, but it creates a massive distributed systems headache known as the Dual-Write Problem.
What happens if you save to the database successfully, but the network blips before you can publish to Kafka? Your database says the user is active, but the Email Service never got the event to send the welcome email. Your system is now inconsistent.
If you reverse the order (publish to Kafka first, then save to the database), what if the database crashes? The Email Service sends the welcome email, but the user isn't actually active in your database!
Because the database and the message broker are two separate systems, you cannot wrap them in a single, atomic transaction.
What is the core problem the Outbox Pattern is designed to solve?
The Solution: The Transactional Outbox
The Outbox Pattern solves the dual-write problem by using the database's own transaction guarantees to ensure the event is captured safely before it's sent.
Analogy: Imagine writing a physical letter. Instead of writing it and immediately walking out into a blizzard to drop it in a mailbox (where you might slip and drop it), you write the letter and put it in a literal "Outbox" tray on your desk. A dedicated mail carrier periodically checks your desk tray and guarantees they will deliver whatever is in there.
How It Works
Instead of trying to talk to the database and Kafka at the same time, your application only talks to the database.
- You start a standard SQL transaction.
- You update your business table (e.g.,
users). - The trick: In the exact same transaction, you insert a record into a new table called
outbox. This record contains the JSON payload of the event you want to send. - You commit the transaction.
Because both inserts happen in the same SQL transaction, they are atomic. Either the user is updated AND the event is saved in the outbox, or neither happens.
In the Outbox Pattern, why is inserting into the outbox table in the same transaction as the business update so important?
Getting the Messages Out
Now you have a database table full of events. How do they get to Kafka?
1. Polling Publisher:
A separate background worker constantly polls the outbox table (SELECT * FROM outbox WHERE sent = false). It reads the rows, publishes them to Kafka, and then marks the rows as sent (or deletes them).
2. Change Data Capture (CDC):
Polling the database every second can degrade performance. A more reliable solution is to use a Change Data Capture tool like Debezium. Debezium hooks directly into the database's transaction log (like PostgreSQL's WAL). Whenever a new row is inserted into the outbox table, Debezium instantly reads it from the log and streams it directly to Kafka.
Change Data Capture (CDC) tools like Debezium read from the database's transaction log rather than polling the outbox table directly.
Which of the following is a valid reason to prefer CDC over a polling publisher?
The "At-Least-Once" Catch
The Outbox pattern guarantees your message will be sent, but it often guarantees At-Least-Once delivery.
If the background worker publishes the message to Kafka, but crashes right before it can mark the row as "sent" in the database, the worker will reboot, read the row again, and publish it to Kafka a second time.
Because of this, all downstream services listening to these events must be Idempotent (they must be able to process the same event twice without causing side effects, usually by tracking the unique Event ID).
The Outbox Pattern guarantees exactly-once delivery to downstream consumers.
Summary
- The Dual-Write Problem occurs when a service must update a database and publish an event simultaneously without atomic guarantees.
- The Outbox Pattern solves this by saving the event payload to a dedicated
outboxtable within the same database transaction as the business data. - A separate process (like a polling worker or Debezium) reads the outbox table and guarantees the messages are forwarded to the message broker.
- It guarantees delivery but requires downstream consumers to be idempotent to handle potential duplicate messages.
Saved on this device only
Sign in to sync progress across devices