Transactional Outbox Pattern: A Practical Way to Publish Reliable Events
In distributed backends, one common problem is this:
A service writes something important to its database, and another service needs to react to it.
For example:
Real-world examples
1. Order placed → Payment / Inventory / Notification
When an order is created, the Order Service writes:
orders table
outbox_events table: ORDER_PLACED
Then other services consume the event:
Order Service → Payment Service
Order Service → Inventory Service
Order Service → Notification Service
2. User registered → Welcome email / onboarding
When a new user signs up, the User Service writes:
users table
outbox_events table: USER_REGISTERED
Then downstream services react:
User Service → Email Service
User Service → CRM Service
User Service → Analytics Service
3. Image batch completed → Email confirmation
When an image upload API receives and processes a batch of images, it writes:
image_batches table
images table
outbox_events table: IMAGE_BATCH_COMPLETED
Then the Email Service sends a confirmation email to the user.
Upload Service → Email Service
Upload Service → Moderation Service
Upload Service → Notification Service
Banking-style example
Imagine a user transfers money from Account A to Account B.
The backend must update balances, create a ledger entry, and notify other systems that the transfer was posted.
Instead of updating the database and separately publishing an event, we write both the business data and the outbox event in the same transaction.
BEGIN;
UPDATE accounts
SET balance = balance - 10000
WHERE id = 'account_A'
AND balance >= 10000;
UPDATE accounts
SET balance = balance + 10000
WHERE id = 'account_B';
INSERT INTO ledger_entries (
transfer_id,
debit_account_id,
credit_account_id,
amount,
status
)
VALUES (
'transfer_123',
'account_A',
'account_B',
10000,
'POSTED'
);
INSERT INTO outbox_events (
event_type,
aggregate_id,
payload,
status,
created_at
)
VALUES (
'TRANSFER_POSTED',
'transfer_123',
'{"from":"account_A","to":"account_B","amount":10000}',
'PENDING',
now()
);
COMMIT;
Now the database has committed both:
ledger_entries table → transfer_123 was posted
outbox_events table → TRANSFER_POSTED is waiting to be published
After that, an outbox worker can pick the pending event and publish it to a broker such as Kafka, RabbitMQ, Redis Streams, or another messaging system.
Then other services can react:
TRANSFER_POSTED
→ Notification Service sends SMS/email
→ Statement Service updates transaction history
→ Fraud Service reviews the transaction
→ Analytics Service updates reporting
Simple timeline
1. API receives request
2. Service writes business data
3. Service writes outbox event in the same DB transaction
4. Transaction commits
5. Outbox worker reads pending events
6. Worker publishes event to message broker
7. Consumer services process the event
8. Event is marked as processed
Why not just publish directly?
Without outbox, this can happen:
Database update succeeds
Event publish fails
Now the system has changed state, but no other service knows about it.
The transactional outbox pattern avoids this by making the event part of the same database transaction.
Conclusion
Transactional outbox gives you fire-and-forget style decoupling, but with better reliability.
The service does not directly wait for Payment, Email, Inventory, or Notification services. It simply records a business event safely, and the event can be retried until it is processed.
A good rule of thumb:
Use transactional outbox when one service writes important data to its own database, and another service must reliably react to that change.
It is ideal for orders, payments, user registration, image processing, emails, notifications, audit logs, and event-driven workflows where losing an event is not acceptable.