Distributed Transactions and the Saga Pattern cover image

Distributed Transactions and the Saga Pattern

By Julian Krause · Wednesday, October 1, 2025 · ~3 min read

In a monolith, maintaining data consistency is straightforward — wrap everything in a database transaction and let ACID guarantees do the work. In a microservices architecture, your data is distributed across multiple databases owned by different services. Distributed transactions using two-phase commit are theoretically possible but practically unworkable at scale due to latency, availability impact, and the coupling they introduce. The saga pattern is the established alternative.

What Is a Saga?

A saga is a sequence of local transactions, where each transaction updates data within a single service. If one step fails, compensating transactions undo the changes made by preceding steps. There are two main coordination strategies: choreography and orchestration.

Choreography — Event-Driven Sagas

In choreography, each service publishes domain events and listens for events from other services. There is no central coordinator.

// Order Service — publishes event after creating order
public class CreateOrderHandler(AppDbContext db, IEventBus eventBus)
    : IRequestHandler<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
    {
        var order = new Order(cmd.CustomerId, OrderStatus.Pending);
        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);

        await eventBus.PublishAsync(new OrderCreatedEvent(order.Id, cmd.Items), ct);
        return order.PublicId;
    }
}

// Inventory Service — listens and reacts
public class OrderCreatedHandler(InventoryDbContext db, IEventBus eventBus)
    : IEventHandler<OrderCreatedEvent>
{
    public async Task Handle(OrderCreatedEvent @event, CancellationToken ct)
    {
        var reserved = await TryReserveStock(@event.Items, ct);

        if (reserved)
            await eventBus.PublishAsync(new StockReservedEvent(@event.OrderId), ct);
        else
            await eventBus.PublishAsync(new StockReservationFailedEvent(@event.OrderId), ct);
    }
}

Choreography works well for simple sagas with few steps. Beyond three or four steps, the event chains become difficult to trace and debug.

Orchestration — Centralized Saga Coordination

In orchestration, a saga orchestrator manages the sequence of steps and handles failures. This is easier to understand and debug but introduces a single point of coordination.

public class OrderSagaOrchestrator(
    IOrderService orderService,
    IInventoryService inventoryService,
    IPaymentService paymentService)
{
    public async Task<SagaResult> ExecuteAsync(CreateOrderRequest request, CancellationToken ct)
    {
        var orderId = Guid.Empty;

        try
        {
            // Step 1: Create order
            orderId = await orderService.CreateOrderAsync(request, ct);

            // Step 2: Reserve inventory
            await inventoryService.ReserveStockAsync(orderId, request.Items, ct);

            // Step 3: Process payment
            await paymentService.ChargeAsync(orderId, request.Total, ct);

            // Step 4: Confirm order
            await orderService.ConfirmOrderAsync(orderId, ct);

            return SagaResult.Success(orderId);
        }
        catch (Exception ex)
        {
            // Compensating transactions in reverse order
            await paymentService.RefundAsync(orderId, ct);
            await inventoryService.ReleaseStockAsync(orderId, ct);
            await orderService.CancelOrderAsync(orderId, ct);

            return SagaResult.Failure(ex.Message);
        }
    }
}

Compensating Transactions Are Not Rollbacks

This is a critical distinction. A compensating transaction is a new transaction that semantically undoes the effect of a previous transaction. It does not restore the exact previous state — it applies a corrective action. A payment refund is not the same as the payment never happening. The payment and refund are both recorded in the audit trail.

Idempotency Is Essential

Network failures, retries, and duplicate message delivery are facts of life in distributed systems. Every saga step and every compensating transaction must be idempotent — processing the same message twice must produce the same result as processing it once. Use idempotency keys and deduplication checks at every boundary.

Sagas add significant complexity. Before reaching for them, confirm that you truly need data distributed across services. If two pieces of data must always be consistent, they probably belong in the same service.


Comments (3)

Hannah Friday, March 6, 2026 5:08 PM

I have a question: does this also apply to older versions?

Clara Tuesday, March 3, 2026 8:08 PM

Interesting thought, thanks for adding that.

David Saturday, March 7, 2026 11:08 PM

Exactly! I had the same thought.

Anna Saturday, March 7, 2026 5:08 PM

Thanks for sharing — very helpful.

Ben Sunday, March 8, 2026 5:08 PM

Could you elaborate on this topic in a follow-up post?

Leave a Comment