Transactional outbox
Recording outgoing messages in the same database transaction as the state change, then delivering them separately, to avoid the dual-write problem.
Reliability
Learn it
After a state change you often must tell another system: publish an event, enqueue a job, send an email. That's two writes to two systems, and no transaction spans both.
Commit first and crash before publishing: the message is lost. Publish first and the commit fails: you announced something that never happened. This is the dual-write problem.
The outbox makes the second write part of the first:
- In the same transaction as the state change,
INSERTa row into anoutboxtable describing the message. - Commit. The change and the intent to notify are durable together, or neither exists.
- A relay (a polling worker, or change-data-capture on the table) reads unsent rows, delivers them, and marks them sent.
- In the same transaction as the state change,
Check
The relay delivers a message, then crashes before marking its row sent. What happens?The same trick works in reverse as an inbox: a consumer records incoming message IDs in the same transaction as their effects, deduplicating at-least-once input. Outbox plus inbox gives effectively-once processing between services without distributed transactions.
Ordering: a single relay processing rows in insertion order preserves order per source; parallel relays need ordering per key.
Quick reference
The same ideas, condensed for revision.
How it goes wrong
- Relay without dedupe downstream
- Redelivery after a relay crash duplicates effects.
- Outbox growth
- Sent rows are never deleted, and the relay's scan slows down.
- Stuck relay
- Messages accumulate unsent; alert on the age of the oldest unsent row.
Instead, consider
- Publish after commit and reconcile
- Occasional loss is detected and repaired by a sweep anyway.
- Change data capture on business tables
- Consumers can work from row changes directly, without explicit messages.
- Jobs table as the queue
- The consumer is your own worker; the 'outbox' row simply is the job.
In practice
- Outbox table + polling relay
- SELECT … FOR UPDATE SKIP LOCKED over unsent rows.
- Debezium / logical replication
- Stream outbox inserts from the database log.
- Framework support
- Many service frameworks ship outbox and inbox implementations.
It assumes
- The state change lives in a database that can also hold the outbox table.
- Consumers deduplicate by message ID or are idempotent.
- Some delay between commit and delivery (typically sub-second to seconds) is acceptable.
Explain it in your own words
Where you practise it
Further reading
Engineers describing it in systems they run.
- Scaling Memcache at Facebook
Meta (Facebook) · Rajesh Nishtala and others · Paper, Apr 2013
The reference on running a look-aside cache hard: leases, invalidation from the commit log, failover without hammering the database, and consistency across regions.
Related concepts
- Transactions
Grouping several reads and writes so they take effect all together or not at all, isolated from concurrent work to a defined degree.
- Message queues
A durable buffer between producers and consumers that hands each message to one consumer at a time and redelivers it unless acknowledged.
- Idempotency
Designing an operation so that performing it twice has the same effect as performing it once, which is what makes retries safe.
- Delivery guarantees
At-most-once, at-least-once, and why 'exactly-once' is achieved by making duplicates harmless rather than by preventing them.