The outbox pattern in .NET: why we use it for every project

There's a class of bug that's almost invisible until it causes a customer support ticket: the application saved the data, but the email didn't send, the webhook didn't fire, or the downstream process never ran. The cause is almost always the same — a gap between the database commit and the side effect.

The problem

The naive implementation looks like this: save the order to the database, then send the confirmation email. If the process crashes between the commit and the email send, the order exists but the customer never heard from you. Retry logic helps but creates duplicates. Transactions don't span external services.

The outbox pattern

The solution is to write the intent to send the email into the same database transaction as the order. A separate background process reads the outbox table and processes messages. If it fails, it retries. If it succeeds, it marks the message as processed. The order and the email are now atomically linked.

Outbox message processing in .NET

public class OutboxProcessor : BackgroundService {
    protected override async Task ExecuteAsync(CancellationToken ct) {
        while (!ct.IsCancellationRequested) {
            var messages = await _repo.GetUnprocessedAsync(batchSize: 20, ct);

            foreach (var msg in messages) {
                try {
                    await _dispatcher.DispatchAsync(msg.Type, msg.Payload, ct);
                    await _repo.MarkProcessedAsync(msg.Id, ct);
                } catch (Exception ex) {
                    await _repo.IncrementRetryAsync(msg.Id, ex.Message, ct);
                }
            }
            await Task.Delay(TimeSpan.FromSeconds(5), ct);
        }
    }
}

Why we use it everywhere

Every engagement we take on ships with an outbox implementation. The overhead is minimal — it's a table, a background job, and a dispatch interface. The benefit is eliminating an entire category of subtle, hard-to-reproduce data consistency bugs. That's a trade we make every time.

Related posts

Apr 2026

Why your Lovable MVP will need a rebuild (and how to avoid it)

Read →

Mar 2026

Multi-tenancy in .NET: what it actually takes to do it right

Read →

Mar 2026

Production-ready vs demo-ready: the 7 systems every SaaS needs before launch

Read →