Transactional Outbox

Reliably publish events to a message broker from a transactional database.

Context

You want to "save the order and publish OrderPlaced to the broker." If you do this in two steps, you might save the order and crash before publishing — or vice versa.

Problem

DB and broker are two independent resources. There's no common transaction.

Solution

Save the event to an outbox table within the same transaction as the aggregate. A separate process reads the table and publishes the pending events.

#C# Example

public class OrderService
{
    public async Task PlaceAsync(Order order)
    {
        await using var tx = await _db.Database.BeginTransactionAsync();
        _db.Orders.Add(order);
        _db.OutboxMessages.Add(new OutboxMessage {
            Type = nameof(OrderPlaced),
            Payload = JsonSerializer.Serialize(new OrderPlaced(order.Id))
        });
        await _db.SaveChangesAsync();        // both in the SAME transaction
        await tx.CommitAsync();
    }
}

// Separate Worker:
public class OutboxDispatcher : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var pending = await _db.OutboxMessages.Where(m => !m.Processed).Take(100).ToListAsync(ct);
            foreach (var m in pending)
            {
                await _bus.PublishAsync(m.Type, m.Payload, ct);
                m.Processed = true;
            }
            await _db.SaveChangesAsync(ct);
            await Task.Delay(500, ct);
        }
    }
}
Tradeoffs
Pro Con
At-least-once guarantee with consistent data Consumers must be idempotent
Does not require distributed transaction Small delay between commit and publish

#extra #messaging #reliability