← Back to blog

2026-07-02

Transactional Outbox Pattern for Reliable Event-Driven Backends

A practical look at using the transactional outbox pattern to reliably publish events after database changes in distributed backends.

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.