RiaChoi Text Logo.
Saga Patterns

Saga

ACID

Saga Patterns

What is a Saga pattern? Orchestrated and Choreographic Saga Patterns

Ria ChoiAugust 18th, 2026

What is the SAGA Pattern?

The SAGA pattern is a way to manage a long transaction by breaking it into a series of smaller steps, each with its own way to undo itself if something goes wrong later.

It was first introduced in a 1987 paper called "Sagas," written by Hector Garcia Molina and Kenneth Salem.

The reason I’m sharing where the word originated is that I’ve been searching for a few minutes, I first thought this word, SAGA, stood for something, but it didn’t. - It was definitely not Candy Crush SAGA

 

Before we move on to the SAGA pattern, I want to introduce you to the concept of ACID.

 

ACID

ACID stands for four principles that ensure database transactions are processed safely: Atomicity, Consistency, Isolation, and Durability.

Thanks to these principles, a transaction won't leave data in a broken state even if it fails partway through, and once completed, the changes are stored permanently.

 

Atomicity ⚛️

Atomicity means a transaction is treated as a single, all-or-nothing unit: either every step in it completes, or none of them do.

  • When you transfer money between two bank accounts, if the deduction from one account succeeds but the addition to the other account fails, atomicity ensures the whole transfer is rolled back so no money disappears.

 

Consistency 🚶🏾‍♀️‍➡️🚶🏾‍➡️🚶🏾‍♀️‍➡️🚶🏾

Consistency means a transaction must move the database from one valid state to another, following all defined rules like constraints and relationships.

  • If a rule says an account balance can't go below zero, a transaction that would break that rule gets stopped before it happens.

 

Isolation 🎫

Isolation means transactions running at the same time don't interfere with each other, as if each one were running alone.

  • If two people book the last seat on a flight at the same moment, isolation ensures only one booking succeeds and the other sees an updated, correct seat count.

 

Durability 📀

Durability means once a transaction is completed, its changes stay saved even if the system crashes right after.

  • If you complete an online purchase and the server loses power a second later, durability guarantees your order is still recorded when the system comes back up.

 

Why do we use Saga pattern?

In a traditional monolithic system, a single database is used, so ensuring ACID compliance isn't very difficult since the database itself provides transaction features. However, in an MSA environment, each service has its own database, which makes it much harder to apply the same rule consistently across all of them. This challenge is known as the distributed transaction problem.

 

That's where the Saga pattern comes in handy.

It mimics ACID's atomicity by using compensating transactions when something fails, making the whole process appear as if it either fully succeeded or fully cancelled. It doesn't provide the same strong, immediate isolation that real ACID guarantees, but instead achieves a looser form of consistency called eventual consistency.

 

Orchestrated Saga Pattern

A central orchestrator calls each service in sequence, and if something fails, it runs compensating transactions.

 

Hooking up ErrorException on the client call
↓
Catch the ErrorException in the orchestration logic

 

1. Orchestrator - core logic

@Service
@RequiredArgsConstructor
public class OrderSagaOrchestrator {

    private final OrderServiceClient orderServiceClient;
    private final PaymentServiceClient paymentServiceClient;
    private final InventoryServiceClient inventoryServiceClient;

    public void executeOrderSaga(OrderRequest request) {
        Long orderId = null;
        Long paymentId = null;

        try {
            // Step 1: Create order
            orderId = orderServiceClient.createOrder(request);

            // Step 2: Process payment
            paymentId = paymentServiceClient.processPayment(orderId, request.getAmount());

            // Step 3: Reserve stock
            inventoryServiceClient.reserveStock(orderId, request.getProductId(), request.getQuantity());

            // All steps succeeded, mark order as complete
            orderServiceClient.completeOrder(orderId);

        } catch (PaymentFailedException e) {
            // Payment failed, cancel the order only
            compensateOrder(orderId);
            throw new SagaExecutionException("Saga rolled back due to payment failure", e);

        } catch (InventoryReservationException e) {
            // Inventory failed, refund payment and cancel order (compensate in reverse order)
            compensatePayment(paymentId);
            compensateOrder(orderId);
            throw new SagaExecutionException("Saga rolled back due to insufficient stock", e);
        }
    }

    private void compensateOrder(Long orderId) {
        if (orderId != null) {
            orderServiceClient.cancelOrder(orderId);
        }
    }

    private void compensatePayment(Long paymentId) {
        if (paymentId != null) {
            paymentServiceClient.refundPayment(paymentId);
        }
    }
}

 

2. Service call client (shown for one)

@Component
@RequiredArgsConstructor
public class PaymentServiceClient {

    private final RestTemplate restTemplate;

    public Long processPayment(Long orderId, BigDecimal amount) {
        try {
            PaymentResponse response = restTemplate.postForObject(
                "http://payment-service/api/payments",
                new PaymentRequest(orderId, amount),
                PaymentResponse.class
            );
            return response.getPaymentId();
        } catch (Exception e) {
            throw new PaymentFailedException("Payment processing failed", e);
        }
    }

    public void refundPayment(Long paymentId) {
        restTemplate.postForObject(
            "http://payment-service/api/payments/" + paymentId + "/refund",
            null, Void.class
        );
    }
}

 

🔎 Code explanation

  • OrderSagaOrchestrator acts as the central brain that controls the whole flow. It explicitly manages the "order then payment then inventory" sequence in a single class.
  • Each step is wrapped in a try catch block, and if a specific step fails, the compensation logic (compensateOrder, compensatePayment) is called to undo the previous steps in reverse order.
  • If the inventory reservation fails, the flow rolls back backward: refund payment, then cancel order. That's the key idea here.
  • Each service client (OrderServiceClient, PaymentServiceClient, InventoryServiceClient) is just a simple client calling a REST API; it has no awareness of the saga's overall flow. Only the orchestrator sees the full picture.
  • Advantage: you can understand the entire business flow just by looking at OrderSagaOrchestrator alone.
In practice, this orchestrator is often implemented as a state machine (using Spring StateMachine) or built with workflow engines like Temporal or Camunda. The code above is a simplified version to illustrate the concept.

 

Choreographic Saga Pattern

There's no central coordinator here. Each service publishes and subscribes to events, triggering the next step on its own. This example assumes Kafka as the message broker.

 

Passing states from one service to another by publishing an event, and the subscribed service notices the publication.

 

1. Order service - publishes an event after creating the order

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final KafkaTemplate<String, Object> kafkaTemplate;

    @Transactional
    public void createOrder(OrderRequest request) {
        Order order = orderRepository.save(Order.from(request));

        // Publish order created event
        kafkaTemplate.send("order-created-topic",
            new OrderCreatedEvent(order.getId(), request.getAmount(), request.getProductId(), request.getQuantity())
        );
    }

    // Receive compensation event (e.g. due to lack of stock), cancel the order
    @KafkaListener(topics = "order-cancellation-topic")
    public void handleOrderCancellation(OrderCancelledEvent event) {
        Order order = orderRepository.findById(event.getOrderId())
            .orElseThrow();
        order.cancel();
        orderRepository.save(order);
    }
}

 

2. Payment service - subscribes to the order event, processes payment, and publishes the next event

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final KafkaTemplate<String, Object> kafkaTemplate;

    @KafkaListener(topics = "order-created-topic")
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            Payment payment = processPayment(event.getOrderId(), event.getAmount());

            // Payment succeeded, trigger the next step (inventory)
            kafkaTemplate.send("payment-completed-topic",
                new PaymentCompletedEvent(event.getOrderId(), event.getProductId(), event.getQuantity())
            );

        } catch (PaymentFailedException e) {
            // Payment failed, publish order cancellation event (compensation)
            kafkaTemplate.send("order-cancellation-topic",
                new OrderCancelledEvent(event.getOrderId(), "Payment failed")
            );
        }
    }

    // Receive compensation event due to insufficient stock, refund the payment
    @KafkaListener(topics = "inventory-reservation-failed-topic")
    public void handleInventoryFailed(InventoryReservationFailedEvent event) {
        refundPayment(event.getOrderId());

        // After refunding, chain a trigger to also cancel the order
        kafkaTemplate.send("order-cancellation-topic",
            new OrderCancelledEvent(event.getOrderId(), "Insufficient stock")
        );
    }

    private Payment processPayment(Long orderId, BigDecimal amount) {
        // Payment logic (throws PaymentFailedException on failure)
        Payment payment = new Payment(orderId, amount);
        return paymentRepository.save(payment);
    }

    private void refundPayment(Long orderId) {
        paymentRepository.findByOrderId(orderId)
            .ifPresent(payment -> {
                payment.refund();
                paymentRepository.save(payment);
            });
    }
}

 

3. Inventory service - subscribes to the payment completed event and processes stock

@Service
@RequiredArgsConstructor
public class InventoryService {

    private final InventoryRepository inventoryRepository;
    private final KafkaTemplate<String, Object> kafkaTemplate;

    @KafkaListener(topics = "payment-completed-topic")
    public void handlePaymentCompleted(PaymentCompletedEvent event) {
        boolean reserved = inventoryRepository.reserveStock(
            event.getProductId(), event.getQuantity()
        );

        if (reserved) {
            // Stock reserved successfully, publish saga completed event
            kafkaTemplate.send("order-completed-topic",
                new OrderCompletedEvent(event.getOrderId())
            );
        } else {
            // Insufficient stock, publish compensation event (payment service subscribes to this)
            kafkaTemplate.send("inventory-reservation-failed-topic",
                new InventoryReservationFailedEvent(event.getOrderId())
            );
        }
    }
}

🔎 Code explanation

  • There is no central coordinator. Each service only subscribes to the events it cares about (@KafkaListener), does its own job, and publishes the next event.
  • The flow unfolds like this:
    OrderService publishes order-created-topic, then PaymentService subscribes to it, processes payment, and publishes payment-completed-topic, then InventoryService subscribes to that and handles the stock.
  • The failure and compensation flow is also chained through events: insufficient stock leads InventoryService to publish inventory-reservation-failed-topic, which PaymentService subscribes to, refunds the payment, and publishes order-cancellation-topic, which OrderService subscribes to and cancels the order.
  • Each service only knows its own logic; no single class explicitly shows what the overall saga flow looks like. This is why documentation and diagrams become important.
  • Advantage: services are loosely coupled, and adding a new service (like a "notification service") requires no changes to existing services, just a new subscription to the relevant event.
  • Disadvantage: reading this code alone doesn't reveal the full picture of "order then payment then inventory." You have to open several files to understand the entire flow. This is a well known drawback of choreography.

 

In practice, teams often mix both approaches (for example, using orchestration for the core flow while handling side features like notifications or logging through choreography).

I’m going to be back with a post elaborating on Kafka and the Outbox pattern.

Share

Related contents