A saga is a sequence of local transactions that are coordinated to achieve a distributed transaction. If any local transaction fails, the saga executes compensating transactions to undo the preceding transactions.

Let’s see a saga in action with Temporal.io. Imagine we’re building an e-commerce order processing system. When a customer places an order, we need to:

  1. Charge the customer’s payment.
  2. Update inventory.
  3. Ship the order.

These are our local transactions. If any of these fail, we need to compensate. For example, if shipping fails after the payment was charged and inventory updated, we need to refund the customer and mark the inventory as available again.

Here’s a simplified Java example using the Temporal SDK:

// Define the workflow interface
public interface OrderSagaWorkflow {
    @WorkflowMethod
    void processOrder(OrderDetails orderDetails);
}

// Implement the workflow
public class OrderSagaWorkflowImpl implements OrderSagaWorkflow {

    private final PaymentService paymentService = new PaymentServiceImpl();
    private final InventoryService inventoryService = new InventoryServiceImpl();
    private final ShippingService shippingService = new ShippingServiceImpl();

    @Override
    public void processOrder(OrderDetails orderDetails) {
        // Step 1: Charge payment
        try {
            paymentService.charge(orderDetails.getPaymentInfo(), orderDetails.getAmount());
            // Record activity completion for compensation
            Workflow.heartbeat("Payment Charged");
        } catch (PaymentFailedException e) {
            // No compensation needed here as nothing was done yet.
            throw e; // Workflow will fail
        }

        // Step 2: Update inventory
        try {
            inventoryService.updateStock(orderDetails.getItems(), -1); // Decrement stock
            Workflow.heartbeat("Inventory Updated");
        } catch (InventoryUpdateFailedException e) {
            // Compensate payment
            paymentService.refund(orderDetails.getPaymentInfo(), orderDetails.getAmount());
            Workflow.heartbeat("Payment Refunded (Compensation)");
            throw e; // Workflow will fail
        }

        // Step 3: Ship order
        try {
            shippingService.ship(orderDetails.getShippingAddress(), orderDetails.getItems());
            Workflow.heartbeat("Order Shipped");
        } catch (ShippingFailedException e) {
            // Compensate inventory and payment
            inventoryService.updateStock(orderDetails.getItems(), 1); // Increment stock back
            Workflow.heartbeat("Inventory Restocked (Compensation)");
            paymentService.refund(orderDetails.getPaymentInfo(), orderDetails.getAmount());
            Workflow.heartbeat("Payment Refunded (Compensation)");
            throw e; // Workflow will fail
        }
    }
}

// Activity interfaces (simplified)
public interface PaymentService {
    void charge(PaymentInfo paymentInfo, double amount);
    void refund(PaymentInfo paymentInfo, double amount);
}

public interface InventoryService {
    void updateStock(List<Item> items, int quantityChange);
}

public interface ShippingService {
    void ship(ShippingAddress address, List<Item> items);
}

Temporal.io manages the state of this workflow. If processOrder is running and the application restarts, Temporal will resume it from where it left off, ensuring that even if the server crashes, the saga progresses. The Workflow.heartbeat() calls are crucial. They record the progress within the workflow’s state. If the workflow fails, Temporal can examine these heartbeats to understand which steps have completed and which compensating actions are necessary.

The core problem Temporal.io solves here is state management for distributed transactions in the face of failures. Traditional distributed transaction protocols (like two-phase commit) are often rigid and don’t handle network partitions or service outages gracefully. Sagas, orchestrated by a durable system like Temporal, offer more resilience. Temporal guarantees that your workflow code will eventually execute to completion, either by successfully completing all steps or by executing all necessary compensating actions.

The PaymentService, InventoryService, and ShippingService would be implemented as Temporal Activities. These are the actual units of work that interact with external systems. Temporal invokes these activities and handles retries, timeouts, and state persistence. The workflow orchestrates these activities, deciding when to call them and what to do if they fail.

A common misunderstanding is that Temporal handles the compensation logic automatically without you defining it. This is not true. You, as the developer, must explicitly write the compensating actions within your workflow code. Temporal’s role is to ensure that this code is executed reliably, even if failures occur. The try-catch blocks in the example demonstrate this explicit compensation. When an InventoryUpdateFailedException is caught, the code explicitly calls paymentService.refund(). Temporal ensures this refund activity will be executed if the inventory update fails, and if the refund activity itself fails, Temporal will retry it according to its configuration.

When you design your saga, think about the "undo" for each step. For charging a payment, the undo is a refund. For decrementing inventory, the undo is incrementing it. For shipping, the undo might involve cancelling a shipment request and then incrementing inventory. The order of compensation is critical: you must undo steps in the reverse order they were performed.

The next concept you’ll encounter is handling complex branching logic within sagas, where different paths might need different compensation strategies, often managed through Temporal’s signal and query mechanisms.

Want structured learning?

Take the full Saga-pattern course →