Reliability
Outbox Pattern
A database commit and a message publish can't be one atomic operation across two systems, and the outbox pattern is how you get the guarantee anyway, by only ever making one of them atomic and letting the other catch up.
ReliabilityAdvanced6 min readPrerequisites: Event-Driven Architecture, Message Queues, Idempotency Keys
After this lesson, you should be able to:
- Explain the dual-write problem: why a database write and a message publish can't be made atomic as two separate operations.
- Describe how the outbox pattern gets atomicity anyway, an event insert in the same transaction as the data write, relayed afterward.
- Diagnose what happens when a relay crashes after publishing but before marking a row published: redelivery, not loss.
- Recognize that outbox trades a silent-loss failure mode for an at-least-once one, which is why consumers still need idempotency.
At a Glance
- Writing to a database and publishing a message are two separate operations against two separate systems. A crash between them, or a publish that just fails, leaves the database right and the event never sent.
- The outbox pattern writes the event into a table in the same transaction as the actual data, so the write and the intent to publish commit atomically, both or neither.
- A separate relay process does the actual publish afterward. The hard part moves there: the relay can crash or double-publish, trading "might silently lose the event" for "consumers must handle at-least-once delivery."
01 The Problem
An order service inserts a row into orders, then calls the message broker to publish "order placed," two separate operations against two separate systems. If the process crashes after the database commit but before the publish call, the order exists and no event ever goes out, shipping never finds out. If the publish call itself just fails, broker down, network blip, same outcome: the order is real, the event isn't sent. Flip the order, publish first and write to the database second, and the failure just moves: now a publish can succeed while the database write fails, and downstream services believe an order exists that the order service itself has no record of.
Wrapping the publish call inside the same "transaction" as the database write doesn't fix this. A message broker doesn't participate in the database's transaction. It has no idea the database is about to commit or roll back, and there's no way to make a database commit and a message publish into a single atomic operation across two different systems, because they're two different systems.
This is the dual-write problem, and it comes up any time a service has to write to its own database and tell the outside world about it, where both have to happen or neither should.
02 What It Is
The outbox pattern makes the write atomic by construction and defers the actual publish to something else. Instead of writing to orders and separately calling the broker, the service writes to orders and inserts a row into an outbox table, in the same database transaction. Both commit together or neither does, because it's one transaction against one database, the thing databases are actually good at guaranteeing. A separate process, the relay, reads unpublished rows from the outbox table, publishes each one to the real broker, and marks it published once the broker confirms.
Writing atomically to one database was the easy problem, the outbox row is just another insert in the same transaction. The hard part moved, it didn't disappear. The relay is now a process reading from the outbox and publishing to the broker, and it can crash between publishing and recording that it published, and on restart it re-reads that same row and publishes it again. The outbox pattern doesn't give you exactly-once publishing. It gives you at-least-once, guaranteed no event is silently lost, at the cost of every consumer downstream needing to handle a duplicate.
In Plain Terms
Dropping a signed letter and a copy of it in your own filing cabinet is one motion, both happen or you never actually finished the motion. Whether that letter actually reaches the post office is a separate question, handled by whoever picks up your outgoing mail and walks it over. If that person forgets, the letter's still sitting in your outbox, visibly unsent, not lost the way it would be if you'd only ever handed it directly to a courier who then vanished before confirming delivery.
03 How It Works
Rendering diagram…
Rendering diagram…
04 Trade-offs
| Direct publish (dual write) | Outbox pattern | |
|---|---|---|
| Atomicity | Database write and publish are two separate operations; either can succeed while the other fails | Database write and outbox insert are one transaction; both commit or neither does |
| Failure mode | An event can be silently lost, with the database showing no trace anything went wrong | An event is never silently lost; worst case it's published more than once |
| Consumer requirement | Assumes each event arrives once, a guarantee the dual-write design can't actually make | Consumers must handle redelivery, since the relay guarantees at-least-once, not exactly-once |
| Moving parts | None beyond the publish call itself | An outbox table plus a relay process that has to stay running and be monitored |
Watch out
It's tempting to think the outbox pattern solves the dual-write problem completely, event delivery is now guaranteed, done. It solves the loss half. It doesn't solve exactly-once, it trades a silent-loss failure mode for a duplicate-delivery one, and that trade only pays off if every consumer downstream actually handles a duplicate. An outbox with a relay publishing to consumers that assume single delivery hasn't fixed the dual-write problem, it's just moved where it breaks, from "the event never showed up" to "the event showed up twice and something double-processed."
05 Where You'll Actually See This
The payment gateway case study writes the charge decision to its Postgres ledger before replying, and that's exactly the kind of write an outbox row would sit next to if the gateway ever needed to notify other internal services, fraud review, accounting, that a charge succeeded. A separate publish call after the ledger commit would reintroduce the exact dual-write gap this article describes.
Event-driven architecture and message queues both start from the moment a message is already published: a service announces what happened, or a producer enqueues something. Neither covers how that first publish actually gets made reliably, and the outbox pattern is the answer underneath "the order service publishes an event," the piece that keeps that step from quietly failing on its own.
The relay's at-least-once delivery is exactly why idempotency keys matters here too, not just on the API side that article covers. A consumer processing an event the relay published twice needs the same defense a synchronous API needs against a client retrying: something that recognizes it's already handled this one instead of assuming every delivery is new.
How to reason about it
When asked how you'd reliably notify other services after a database write, don't just say "publish an event." Say what happens if the process crashes between the write and the publish, because that gap is the actual problem, and "publish an event" on its own doesn't answer it. Naming the outbox table and the relay, and saying what your consumers need to tolerate because of it, is what shows you've thought about the failure mode, not just the happy path.
06 Quick Check
A relay process reads an outbox row, successfully publishes it to the broker, then crashes before it can mark the row as published. What happens to that event?