Current apps frequently have to broadcast an event to a message broker and update a database. An e-commerce program may, for instance, broadcast an OrderCreated event to RabbitMQ, Azure Service Bus, or Kafka after saving an order to SQL Server.
Making sure both operations are successful together is the difficult part. The event is never received by downstream systems if the database transaction commits but event publishing is unsuccessful. On the other hand, customers receive an event for nonexistent data if the event is published but the database transaction fails. By storing events in the same database transaction as the business data and publishing them asynchronously thereafter, the Outbox Pattern resolves this issue.
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.
This ensures reliable event delivery even during temporary messaging outages.
Outbox Pattern vs Direct Publishing
| Feature | Direct Publish | Outbox Pattern |
|
Atomic database update
|
No
|
Yes
|
|
Reliable delivery
|
Limited
|
Yes
|
|
Retry support
|
Manual
|
Built-in
|
|
Event persistence
|
No
|
Yes
|
|
Temporary broker outage handling
|
Poor
|
Excellent
|
|
Suitable for microservices
|
Moderate
|
Excellent
|
Performance Evaluation Methodology
The research brief references reliability and scalability but does not include benchmark results. Instead of presenting unsupported figures, evaluate your implementation using the following methodology.
Test Environment
Keep these variables consistent:
- .NET SDK version
- Database engine
- Message broker
- Hardware
- Network conditions
- Polling interval
Test Scenarios
Compare:
- Direct publishing
- Outbox implementation
- Broker outage
- Database failure
- High event throughput
- Multiple publisher instances
- Metrics to Measure
Collect:
- Events published per second
- Publish latency
- Failed publishes
- Retry count
- Queue backlog
- Database growth
- CPU utilization
- Memory usage
Useful Tools
Useful tools include:
- BenchmarkDotNet (for application components)
- dotnet-counters
- dotnet-trace
- SQL Server Query Store
- RabbitMQ Management UI
- Azure Service Bus metrics
- Kafka monitoring tools
Validate behavior under production-like failure conditions rather than relying only on successful execution paths.
Best Practices
- Store Outbox records in the same transaction as business data.
- Keep event payloads immutable.
- Include unique event identifiers.
- Build idempotent consumers.
- Monitor Outbox table growth.
- Archive or remove processed events periodically.
- Implement retry with exponential backoff if appropriate.
Log publishing failures with sufficient context.
Common Mistakes
| Mistake | Impact |
|
Publishing before committing the database
|
Lost consistency
|
|
Deleting events before successful publish
|
Event loss
|
|
No retry mechanism
|
Failed events remain unpublished
|
|
Non-idempotent consumers
|
Duplicate processing
|
|
Never cleaning the Outbox table
|
Unbounded database growth
|
|
Long polling intervals
|
Increased event latency
|
Troubleshooting
Events Remain in the Outbox Table
Verify:
- Background service is running.
- Message broker is available.
- Publishing logic is not throwing exceptions.
- Database updates are being committed.
Duplicate Events
Review:
- Consumer idempotency.
- Event identifiers.
- Retry logic.
- Publisher crash recovery.
Outbox Table Grows Continuously
Check:
- Cleanup strategy.
- Processing failures.
- Polling frequency.
- Broker health.
Implement scheduled archival or deletion of successfully processed events.
FAQs
Why not publish directly after SaveChanges()?
If publishing fails after the database transaction succeeds, the event is permanently lost. The Outbox Pattern eliminates this inconsistency.
Does the Outbox Pattern guarantee exactly-once delivery?
No. It typically guarantees at-least-once delivery. Consumers should be designed to handle duplicate events safely.
Can the Outbox Pattern work with any message broker?
Yes. It is independent of the messaging platform and can be used with RabbitMQ, Azure Service Bus, Kafka, and other brokers.
How often should the background service poll the Outbox table?
The interval depends on latency requirements and workload. Shorter intervals reduce event latency but increase database activity.
Should processed Outbox records be deleted?
Yes. Archive or delete processed records periodically to prevent the Outbox table from growing indefinitely.
Conclusion
A tried-and-true method for dependable event publication in distributed systems is the Outbox Pattern. It removes the possibility of losing events as a result of transient communications failures by storing business data and integration events within the same database transaction. The Outbox Pattern offers a strong basis for event-driven ASP.NET Core applications and microservice architectures when combined with background processing, retries, idempotent consumers, and routine cleanup.
HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.
