Introduction
In distributed systems, reliability is everything.
When your application updates a database and sends a message to a message broker (like RabbitMQ, Kafka, or Azure Service Bus), there’s a real risk:
✅ The database update succeeds, but the message fails to send.
✅ Or, the message is sent, but the transaction in the database rolls back.
This mismatch causes data inconsistency — one of the hardest bugs to trace in production.
To solve this, enterprise systems adopt the Outbox Pattern, a proven approach to guarantee exactly-once message delivery and data consistency across distributed components.
This article explains how to implement the Outbox Pattern in ASP.NET Core using Entity Framework Core and a message broker (RabbitMQ/Kafka), ensuring reliable message delivery without losing or duplicating data.
1. The Problem: Dual Writes
Let’s say you’re building an Order Service. When a new order is created:
You store it in the database.
You send an “OrderCreated” event to RabbitMQ.
If the database save succeeds but RabbitMQ publishing fails, your downstream services will never know about the order — leading to broken workflows.
Unreliable Flow Example
App → Save to DB (Success)
→ Publish Event (Failed)Your data is now inconsistent.
2. The Solution: Outbox Pattern
The Outbox Pattern ensures that database changes and message publishing happen atomically, even if your message broker or network is temporarily unavailable.
How It Works
When you perform a write operation (like saving an order),
you also insert an event record into an Outbox table within the same database transaction.A background process (Outbox Processor) reads pending messages from the Outbox table and publishes them to the message broker.
Once successfully sent, the message is marked as processed.
3. Technical Workflow (Flowchart)
+-----------------------------+
| Application/API |
+-------------+---------------+
|
v
+-----------------------------+
| Begin Database Transaction |
+-----------------------------+
| 1. Insert Business Data |
| 2. Insert Outbox Event |
+-----------------------------+
| Commit Transaction |
+-----------------------------+
|
v
+-----------------------------+
| Outbox Processor Service |
| Reads Pending Events |
| Publishes to Message Queue |
+-----------------------------+
|
v
+-----------------------------+
| Mark Event as Processed |
+-----------------------------+This guarantees data consistency — either both the data and message are persisted, or neither is.
4. Designing the Outbox Table
Create a simple Outbox table to store events before they’re published.
SQL Table Example
CREATE TABLE OutboxMessages (
Id UNIQUEIDENTIFIER PRIMARY KEY,
EventType NVARCHAR(200),
Payload NVARCHAR(MAX),
CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
ProcessedAt DATETIME2 NULL,
Status NVARCHAR(50) NOT NULL DEFAULT 'Pending'
);
5. Step-by-Step Implementation in ASP.NET Core
Step 1: Define the OutboxMessage Entity
public class OutboxMessage
{
public Guid Id { get; set; } = Guid.NewGuid();
public string EventType { get; set; }
public string Payload { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ProcessedAt { get; set; }
public string Status { get; set; } = "Pending";
}
Step 2: Update Your DbContext
public class AppDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<OutboxMessage> OutboxMessages { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
}
Step 3: Save Data and Add an Outbox Event (Within One Transaction)
public async Task CreateOrderAsync(Order order)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
var eventPayload = JsonSerializer.Serialize(new { order.Id, order.CustomerName, order.TotalAmount });
var outboxMessage = new OutboxMessage
{
EventType = "OrderCreated",
Payload = eventPayload
};
_context.OutboxMessages.Add(outboxMessage);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}

Join the conversation! Your thoughts help the community grow.