Modern applications often need to update a database and publish an event to a message broker. For example, an e-commerce application may save an order to SQL Server and then publish an OrderCreated event to RabbitMQ, Azure Service Bus, or Kafka.
The challenge is ensuring both operations succeed together. If the database transaction commits but event publishing fails, downstream systems never receive the event. Conversely, if the event is published but the database transaction fails, consumers receive an event for data that doesn't exist.
The Outbox Pattern solves this problem by storing events in the same database transaction as the business data and publishing them asynchronously later.
In this article, you'll build a production-ready Outbox implementation in ASP.NET Core, understand how it works, and learn best practices for reliable event publishing.
Note: This article focuses on architecture and implementation. Performance depends on workload, database design, polling intervals, and messaging infrastructure.
What Is the Outbox Pattern?
Instead of publishing an event directly after saving data, the application stores the event in an Outbox table within the same database transaction.
A background service later reads pending events and publishes them to the message broker.
Client Request
│
▼
Business Logic
│
▼
Database Transaction
│
┌────┴─────────┐
▼ ▼
Business Data Outbox Event
│
▼
Transaction Commit
│
▼
Background Publisher
│
▼
Message Broker
This guarantees that if the business transaction succeeds, the event is never lost.
Why Use the Outbox Pattern?
Without the Outbox Pattern:
Database commit succeeds.
Message broker becomes unavailable.
Event is lost.
Downstream services never process the operation.
With the Outbox Pattern:
Business data and event are committed together.
Event publishing can be retried safely.
Temporary messaging failures don't lose events.
Create the Outbox Entity
Create an entity to store pending events.
public class OutboxMessage
{
public Guid Id { get; set; }
public string EventType { get; set; } = "";
public string Payload { get; set; } = "";
public DateTime CreatedAt { get; set; }
public DateTime? ProcessedAt { get; set; }
}
Each row represents one event waiting to be published.
Configure EF Core
Register the Outbox table.
public class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<OutboxMessage> Outbox =>
Set<OutboxMessage>();
public AppDbContext(
DbContextOptions<AppDbContext> options)
: base(options)
{
}
}
Run the migration to create the table.
dotnet ef migrations add AddOutbox
dotnet ef database update
Save Business Data and Event Together
Suppose an order is created.
public async Task CreateOrderAsync(Order order)
{
_context.Orders.Add(order);
var message = new OutboxMessage
{
Id = Guid.NewGuid(),
EventType = "OrderCreated",
Payload = JsonSerializer.Serialize(order),
CreatedAt = DateTime.UtcNow
};
_context.Outbox.Add(message);
await _context.SaveChangesAsync();
}
Both the order and the Outbox record are committed in a single database transaction.
Build the Background Publisher
Create a hosted service that processes pending events.
public class OutboxProcessor
: BackgroundService
{
private readonly IServiceProvider _provider;
public OutboxProcessor(
IServiceProvider provider)
{
_provider = provider;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope =
_provider.CreateScope();
var db = scope.ServiceProvider
.GetRequiredService<AppDbContext>();
var events = await db.Outbox
.Where(x => x.ProcessedAt == null)
.ToListAsync(stoppingToken);
foreach (var message in events)
{
// Publish to broker
message.ProcessedAt =
DateTime.UtcNow;
}
await db.SaveChangesAsync(
stoppingToken);
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
}
}
}
The service periodically publishes pending events and marks them as processed.
Register the Hosted Service
builder.Services.AddHostedService<
OutboxProcessor>();
The publisher starts automatically with the application.
Publishing to a Message Broker
The publishing logic depends on the messaging platform.
await publisher.PublishAsync(
message.EventType,
message.Payload);
The Outbox Pattern works with:
RabbitMQ
Azure Service Bus
Apache Kafka
Amazon SQS
NATS
Redis Streams
The persistence strategy remains the same regardless of the broker.
Handling Failures
If publishing fails:
Keep the Outbox row unchanged.
Retry during the next polling cycle.
Mark the event as processed only after successful publishing.
Avoid deleting events before confirming successful delivery.
Prevent Duplicate Processing
A consumer may receive the same event more than once if publishing succeeds but the application crashes before updating ProcessedAt.
Consumers should therefore be idempotent.
Example strategies include:
Event IDs
Message deduplication
Processed message table
Unique business keys
Idempotency is a critical part of any reliable messaging architecture.
End-to-End Workflow
A complete request follows these steps:
Client submits an order.
Order is saved.
Outbox event is stored.
Database transaction commits.
Background service reads pending events.
Event is published.
Event is marked as processed.
Consumers process the event.

Join the conversation! Your thoughts help the community grow.