Skip to content

// Resilience & Messaging · 03

Saga

Step 1 ✓leads toStep 2 ✗leads toCompensate

No single big transaction. Each step knows how to undo itself.

How it fits together

Saga structureReserveStockReleaseStockChargePaymentSaga orchestratorsaga state by OrderIdtimeout 5 minRefundPaymentCreateShipment1 reserve2 charge3 ship4 step fails5 undo charge6 undo stock
uses / callsfails / error path· numbered steps replay in order
  • Saga orchestrator uses / calls ReserveStock (1 reserve)
  • Saga orchestrator uses / calls ChargePayment (2 charge)
  • Saga orchestrator uses / calls CreateShipment (3 ship)
  • CreateShipment fails / error path Saga orchestrator (4 step fails)
  • Saga orchestrator fails / error path RefundPayment (5 undo charge)
  • Saga orchestrator fails / error path ReleaseStock (6 undo stock)

Spot it

  • Payment taken but stock reservation failed
  • Someone proposes 2PC across microservices
  • Orders stuck half-done after a failure

It costs

No isolation between steps: others see intermediate state, and every undo must be designed.

In code

public sealed class CheckoutSaga(IStock stock, IPayments payments, IShipping shipping){    public async Task RunAsync(Guid orderId, CancellationToken ct)    {        var undo = new Stack<Func<Task>>();        try        {            await stock.ReserveAsync(orderId, ct);            undo.Push(() => stock.ReleaseAsync(orderId, CancellationToken.None));​            await payments.ChargeAsync(orderId, ct);            undo.Push(() => payments.RefundAsync(orderId, CancellationToken.None));​            await shipping.CreateAsync(orderId, ct); // irreversible, so it runs last        }        catch (Exception)        {            while (undo.Count > 0)            {                await undo.Pop()();            }​            throw;        }    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

Without a distributed transaction, each service commits locally and the saga coordinates them. When a step fails, compensating transactions run in reverse to semantically undo the earlier ones: a refund, not a rollback.

The classic mistake is assuming compensation restores the world. A sent email cannot be unsent, and other requests already saw the intermediate state. Put irreversible steps after the pivot transaction, and use semantic locks such as a PENDING status so nobody acts on half-finished data.

Orchestration keeps the flow in one place and is easier to follow; choreography via events has no coordinator but scatters the flow. Either way, each step needs an outbox to emit its result reliably and idempotent handlers, because steps will be retried.

Ask for it

  • orchestration
  • choreography
  • compensating transaction
  • pivot transaction
  • saga state
  • semantic lock

Model checkout as an orchestrated saga: reserve stock, charge payment, create shipment, with compensations release stock and refund payment. Persist saga state in a table keyed by order id, time out a step after 5 minutes, and make every step and compensation idempotent.