ASP.NET Core  

Implementing the Outbox Pattern in ASP.NET Core for Reliable Event Publishing

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:

  1. Client submits an order.

  2. Order is saved.

  3. Outbox event is stored.

  4. Database transaction commits.

  5. Background service reads pending events.

  6. Event is published.

  7. Event is marked as processed.

  8. Consumers process the event.

This ensures reliable event delivery even during temporary messaging outages.

Outbox Pattern vs Direct Publishing

FeatureDirect PublishOutbox Pattern
Atomic database updateNoYes
Reliable deliveryLimitedYes
Retry supportManualBuilt-in
Event persistenceNoYes
Temporary broker outage handlingPoorExcellent
Suitable for microservicesModerateExcellent

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

MistakeImpact
Publishing before committing the databaseLost consistency
Deleting events before successful publishEvent loss
No retry mechanismFailed events remain unpublished
Non-idempotent consumersDuplicate processing
Never cleaning the Outbox tableUnbounded database growth
Long polling intervalsIncreased 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

The Outbox Pattern is a proven solution for reliable event publishing in distributed systems. By persisting business data and integration events within the same database transaction, it eliminates the risk of losing events due to temporary messaging failures.

Combined with background processing, retries, idempotent consumers, and regular cleanup, the Outbox Pattern provides a robust foundation for event-driven ASP.NET Core applications and microservice architectures.