In distributed systems, you often need to update a database and send a message to another service. This is called a dual write, and it's harder than it looks.
If you update the database first and then send the message, the message might fail. Now your database has changed but no one knows about it. If you send the message first and then update the database, the database write might fail. Now another service is acting on something that never happened.
You can't wrap both in a transaction because your database and message broker are separate systems. Distributed transactions exist, but they're slow, complex, and most messaging systems don't support them anyway.
The transactional outbox pattern sidesteps this problem entirely.
Instead of sending the message directly, you write it to an outbox table in the same database as your business data. Same transaction. If the transaction commits, the message is guaranteed to be there. If it rolls back, the message disappears with it.
A separate process then picks up messages from the outbox and delivers them to the message broker. This can be a background job that polls the table, or better, a Change Data Capture (CDC) tool like Debezium that streams changes from the database log. CDC avoids the overhead of polling and works with most popular databases including MySQL, Postgres, and SQL Server.
The consuming service needs to handle messages idempotently since delivery might happen more than once. And your delivery process needs a retry mechanism for when the broker is unavailable.
That's the pattern. Write to the outbox in your transaction. Deliver asynchronously. Retry until it works.
It adds a moving part to your system, but it solves a real problem. Your database and your messages stay in sync without distributed transactions, without tight coupling, and without hoping that two separate systems both succeed at the same time.
Anywhere you need to update a database and notify another system reliably, this pattern is worth knowing.