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)
I have a question: does this also apply to older versions?
Interesting thought, thanks for adding that.
Exactly! I had the same thought.
Thanks for sharing — very helpful.
Could you elaborate on this topic in a follow-up post?
Leave a comment