In this lesson, you will learn how to maintain strict data consistency between your database state and your message broker by implementing the Outbox Pattern.
The Dual Write Problem
When building distributed systems, you often face the "Dual Write" problem: you need to update your database (e.g., mark an order as "Created") and publish a message to a queue (e.g., OrderCreatedIntegrationEvent). If you perform these as two separate operations, your system risks inconsistency. For example, if the database succeeds but the network fails before the message is sent, your downstream services will never know the order exists. You cannot wrap both operations in a single atomic transaction because the database and the message broker are different distributed systems.
The Outbox Solution
The Outbox pattern solves this by adding an Outbox table to your local database. Instead of publishing the message directly to the broker, you insert the message into the Outbox table as part of the same local database transaction that performs your business logic.
Implementation Concept
Within your DbContext transaction, you perform two operations:
- Update your business entity (e.g.,
Order). - Write the integration event to the
Outboxtable.
using (var transaction = await _context.Database.BeginTransactionAsync())
{
_context.Orders.Add(newOrder);
_context.OutboxMessages.Add(new OutboxMessage {
Type = nameof(OrderCreatedEvent),
Payload = JsonSerializer.Serialize(eventData),
OccurredOn = DateTime.UtcNow
});
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
Since both operations exist in the same database, they are committed atomically. If the database update fails, the message is never saved; if the transaction commits, both are guaranteed to exist.
Background Dispatching
To complete the pattern, a BackgroundService periodically polls the Outbox table. It reads the pending messages, publishes them to your message broker (like RabbitMQ or Azure Service Bus), and—upon confirmation of receipt from the broker—marks the messages as "Processed" or removes them from the Outbox table. This decoupling ensures that even if the message broker is temporarily unavailable, your local state remains consistent, and the message will eventually be dispatched once the broker is back online.
Key Takeaway: The Outbox pattern achieves atomic consistency by replacing external message publishing with a local database write, ensuring that messages are only ever sent if the corresponding business transaction succeeds.