Home/Learn/Microservices/SAGA Pattern for Distributed Transactions

SAGA Pattern for Distributed Transactions

Advanced
Data Management

A SAGA splits a multi-service transaction into local transactions linked by events (choreography) or an orchestrator, with compensating transactions for rollback.

Overview

In a microservices architecture, a single business operation (e.g., placing an order) may span multiple services — Order Service, Payment Service, Inventory Service, Notification Service. Each service has its own database, so a traditional distributed ACID transaction (2-Phase Commit) is impractical due to high coupling, availability risk, and performance cost. The SAGA pattern solves this by decomposing the distributed transaction into a sequence of local transactions. Each step publishes an event or message that triggers the next. If any step fails, previously completed steps are undone via compensating transactions. SAGAs embrace eventual consistency rather than strong consistency, which is the correct trade-off for most microservices use cases.

Choreography-Based SAGA

In choreography, there is no central coordinator. Each service listens for events, performs its local transaction, and publishes a new event for the next step. If it fails, it publishes a failure event that triggers compensating transactions in upstream services.

Pros: simple, fully decentralised, no single point of failure. Cons: hard to visualise the overall flow, cyclic dependencies risk, difficult to debug when a failure cascades through many services.

Java — Choreography SAGA
// Order Service — starts the SAGA
@Service
public class OrderService {
    public void placeOrder(OrderRequest req) {
        Order order = orderRepo.save(new Order(req, OrderStatus.PENDING));
        // Publish event → triggers Payment Service
        eventBus.publish(new OrderCreatedEvent(order.getId(), req.getAmount(), req.getUserId()));
    }

    // Compensating transaction — called if payment or inventory fails
    @EventListener
    public void onPaymentFailed(PaymentFailedEvent event) {
        orderRepo.updateStatus(event.getOrderId(), OrderStatus.CANCELLED);
        eventBus.publish(new OrderCancelledEvent(event.getOrderId()));
    }
}

// Payment Service — reacts to OrderCreatedEvent
@Service
public class PaymentService {
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        try {
            Payment payment = paymentGateway.charge(event.getUserId(), event.getAmount());
            paymentRepo.save(payment);
            // Success → triggers Inventory Service
            eventBus.publish(new PaymentCompletedEvent(event.getOrderId()));
        } catch (PaymentException e) {
            // Failure → triggers compensating transaction in Order Service
            eventBus.publish(new PaymentFailedEvent(event.getOrderId()));
        }
    }
}

Orchestration-Based SAGA

In orchestration, a dedicated SAGA Orchestrator (or Process Manager) drives the sequence. It sends commands to each service and listens for replies. The orchestrator holds the overall state and decides the next step based on the reply.

Pros: explicit flow visible in one place, easy to add steps, simpler error handling. Cons: central coordinator is a single point of complexity (though not of failure if designed idempotently), requires more infrastructure.

Java — Orchestration SAGA
// SAGA Orchestrator — explicit state machine
@Service
public class PlaceOrderSaga {

    // Step 1: Reserve inventory
    public void start(PlaceOrderCommand cmd) {
        sagaState.save(new SagaState(cmd.getOrderId(), "RESERVING_INVENTORY"));
        inventoryService.reserve(new ReserveInventoryCommand(cmd.getOrderId(), cmd.getItems()));
    }

    // Step 2: Charge payment (after inventory reserved)
    @SagaOrchestrationStep
    public void onInventoryReserved(InventoryReservedEvent event) {
        sagaState.update(event.getOrderId(), "CHARGING_PAYMENT");
        paymentService.charge(new ChargePaymentCommand(event.getOrderId(), event.getAmount()));
    }

    // Step 3: Confirm order (after payment charged)
    @SagaOrchestrationStep
    public void onPaymentCharged(PaymentChargedEvent event) {
        orderService.confirm(event.getOrderId());
        sagaState.update(event.getOrderId(), "COMPLETED");
    }

    // Compensate: release inventory if payment fails
    @SagaOrchestrationStep
    public void onPaymentFailed(PaymentFailedEvent event) {
        inventoryService.release(event.getOrderId());   // compensating transaction
        orderService.cancel(event.getOrderId());
        sagaState.update(event.getOrderId(), "FAILED");
    }
}

Compensating Transactions & Idempotency

A compensating transaction undoes the effect of a completed step — e.g., a payment refund compensates a successful charge. Compensating transactions must be idempotent: if the message is delivered twice, the outcome must be the same. Use idempotency keys (orderId + step name) stored in a processed-events table to detect and skip duplicate processing.

SAGAs are eventually consistent — there is a window where partial state is visible to other parts of the system. Design the UI and downstream services to handle intermediate states gracefully.

Java — Idempotent Compensation
// Idempotent compensating transaction using a processed-events table
@Service
public class InventoryService {

    @Transactional
    public void releaseInventory(ReleaseInventoryCommand cmd) {
        String idempotencyKey = "release:" + cmd.getOrderId();

        // Skip if already processed (duplicate message delivery)
        if (processedEventRepo.exists(idempotencyKey)) {
            log.info("Skipping duplicate release for order {}", cmd.getOrderId());
            return;
        }

        inventoryRepo.restoreStock(cmd.getOrderId(), cmd.getItems());
        processedEventRepo.save(new ProcessedEvent(idempotencyKey, Instant.now()));
        log.info("Inventory released for order {}", cmd.getOrderId());
    }
}

Key Points to Remember

  • 1SAGA breaks a distributed transaction into local transactions; each step publishes an event (choreography) or responds to a command (orchestration).
  • 2Compensating transactions undo the effect of completed steps — they must be idempotent because messages can be delivered more than once.
  • 3Choreography = decentralised, services react to events; Orchestration = centralised, a coordinator drives the sequence.
  • 4SAGAs provide eventual consistency, not ACID — a failure window exists where partial state is visible across services.
  • 5Use an idempotency key (e.g., orderId + stepName) stored in a DB table to detect and skip duplicate event/command delivery.
  • 6Always design intermediate states (PAYMENT_PENDING, INVENTORY_RESERVED) explicitly in your domain model — they are real business states in a SAGA world.

Interview Questions

Sign in to ask Aria
1

Why can't we use a single ACID transaction across multiple microservices?

EasyAmazon
2

Explain the difference between choreography and orchestration in the SAGA pattern.

MediumUber
3

What is a compensating transaction and why must it be idempotent?

MediumFlipkart
4

A SAGA's payment step succeeds but the notification step fails. How do you handle this?

HardGoogle
5

How do you handle the scenario where a compensating transaction itself fails?

HardNetflix

Ask Aria about SAGA Pattern for Distributed Transactions

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…