Skip to content
Spring Boot sb microservices 4 min read

Saga Pattern

In a monolith, one @Transactional method keeps orders, payments, and inventory consistent — all or nothing. Split those across services with separate databases and that single ACID boundary is gone. The Saga pattern restores consistency without distributed transactions: a business transaction becomes a sequence of local transactions, each with a compensating action that undoes it if a later step fails.

Why not just a distributed transaction?

Two-phase commit (XA) across services is technically possible but practically avoided: it locks resources across the network, couples services to a coordinator, and scales poorly. The microservices answer is to accept eventual consistency and design for compensation instead.

local tx 1: create order (PENDING)
local tx 2: charge payment
local tx 3: reserve inventory   ← fails!
            ↓ compensate in reverse
comp tx 2:  refund payment
comp tx 1:  cancel order

A saga has no global rollback — instead each completed step has an explicit compensating transaction that semantically reverses it.

Two flavors: choreography vs orchestration

AspectChoreographyOrchestration
CoordinationServices react to each other’s eventsA central orchestrator issues commands
CouplingDecentralized, event-drivenCentralized control flow
VisibilityFlow is implicit across servicesFlow is explicit in one place
Best forSimple sagas, few stepsComplex sagas, many steps/branches
RiskCyclic event chains, hard to traceOrchestrator becomes a bottleneck

Choreography saga

Each service publishes events and reacts to others’ events — no central brain. With messaging (see Inter-Service Communication):

// Order service: start the saga
@Service
@RequiredArgsConstructor
public class OrderService {
    private final RabbitTemplate rabbit;

    @Transactional
    public Order place(OrderRequest req) {
        Order order = repository.save(Order.pending(req));
        rabbit.convertAndSend("saga", "order.created",
                new OrderCreated(order.getId(), req.total(), req.items()));
        return order;
    }
}
// Payment service: react, then emit success or failure
@Component
@RequiredArgsConstructor
public class PaymentSaga {
    private final RabbitTemplate rabbit;

    @RabbitListener(queues = "payment.order-created")
    public void on(OrderCreated event) {
        try {
            payments.charge(event.orderId(), event.total());
            rabbit.convertAndSend("saga", "payment.completed",
                    new PaymentCompleted(event.orderId()));
        } catch (PaymentDeclined ex) {
            rabbit.convertAndSend("saga", "payment.failed",
                    new PaymentFailed(event.orderId()));
        }
    }
}
// Order service: compensate on failure
@RabbitListener(queues = "order.payment-failed")
public void onPaymentFailed(PaymentFailed event) {
    orderRepository.markCancelled(event.orderId());   // compensating action
}
order.created → payment.completed → inventory.reserved → order CONFIRMED
order.created → payment.failed     → order CANCELLED (compensate)

Tip: Use a transactional outbox — write the event to an outbox table in the same local transaction as the state change, then a relay publishes it. This prevents the classic bug where the DB commits but the event is lost (or vice versa). Spring Modulith and Debezium both implement this.

Orchestration saga

A dedicated orchestrator owns the flow, issuing commands and reacting to replies. The control logic lives in one readable place:

@Service
@RequiredArgsConstructor
public class OrderSagaOrchestrator {
    private final PaymentClient payments;
    private final InventoryClient inventory;
    private final OrderRepository orders;

    public void execute(Long orderId, Money total, List<Item> items) {
        try {
            payments.charge(orderId, total);          // step 1
            inventory.reserve(orderId, items);        // step 2
            orders.confirm(orderId);                  // success
        } catch (InventoryUnavailable ex) {
            payments.refund(orderId);                 // compensate step 1
            orders.cancel(orderId);
        } catch (PaymentDeclined ex) {
            orders.cancel(orderId);                   // nothing to compensate
        }
    }
}

For non-trivial flows, model the saga as an explicit state machine (e.g. Spring Statemachine) so each state, transition, and compensation is declared rather than buried in try/catch.

Warning: Compensations must be idempotent and tolerant of partial failure — the orchestrator itself can crash mid-saga and resume. Persist saga state after every step so recovery picks up exactly where it left off. See idempotency keys in Inter-Service Communication.

Designing compensations

Not everything can be literally undone. A sent email or a shipped package can’t be “un-sent” — the compensation is a new business action (send an apology, issue a return label).

Forward actionCompensating action
Reserve inventoryRelease reservation
Charge paymentRefund payment
Allocate shipmentCancel shipment
Send confirmation emailSend cancellation email

Eventual consistency

A saga means the system is temporarily inconsistent — the order exists as PENDING before payment and inventory confirm. Embrace this in the UI (“Order received, confirming…”) and in your data model with explicit status fields. Combined with Distributed Tracing, you can follow a saga end-to-end across services by its trace id.

Note: Sagas are not free. If a workflow genuinely needs strong consistency and the steps share an owner, that’s a strong signal the steps belong in one service (or a modular monolith) — not spread across the network.

Last updated June 13, 2026
Was this helpful?