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