Skip to main content

Design a Saga Orchestrator for Distributed Transactions

Problem Statement

Design an Orchestrator-based Saga Pattern engine for distributed transactions (similar to execution coordinators in modern microservice architectures). The engine is responsible for executing a sequence of steps across multiple remote services (e.g., Order creation, Payment deduction, Inventory reservation). If any step in the sequence fails, the orchestrator must rollback the transaction by executing compensatory commands in reverse order for all previously completed steps.

Asked In Companies
Uber Netflix Stripe

Design Decisions & Patterns Used

Managing transactions across independent databases in microservices is highly complex. Since we cannot use database-level two-phase commit (2PC) locks without bottlenecking performance, we apply the Saga Pattern. In an orchestrator-based Saga, a central component coordinates the workflow steps. If a step fails, the orchestrator executes compensations in reverse order (e.g., if step 3 fails, it executes compensation for step 2, then step 1).

We will utilize the following Design Patterns:

  • Command Pattern: Modeling each service step as a transaction command exposing two behaviors: execute() (forward progress) and compensate() (rollback).
  • State Pattern: Tracking transaction lifecycle states (e.g., SUCCESSFUL, FAILED, COMPENSATING) to determine transition rules.
  • Template Method: Defining the high-level transactional execution skeleton while letting subclasses implement service-specific calls.

Functional Requirements

  • Define steps with forward execution and backward compensation routines.
  • Coordinate transaction state transitions: SUCCESSFUL, FAILED, IN_PROGRESS, COMPENSATING.
  • Execute transaction steps sequentially. Abort immediately if any step fails.
  • Trigger compensations in reverse order for all completed steps when a failure occurs.
  • Ensure the orchestrator can handle execution retries if a compensation fails.

Objects Required

  • SagaStatus (Enum tracking execution states)
  • SagaStep (Interface representing a workflow command)
  • OrderCreationStep, PaymentProcessingStep, InventoryReservationStep (Concrete steps)
  • Saga (Model representing a workflow pipeline)
  • SagaOrchestrator (Execution engine coordinating transitions and rollbacks)

SagaStatus Enum & SagaStep Interface

The SagaStatus tracks the transactional state, and the SagaStep interface defines the commands required for forward and backward operations.


public enum SagaStatus {
    SUCCESSFUL,
    FAILED,
    IN_PROGRESS,
    COMPENSATING
}

Let's define the SagaStep interface:


public interface SagaStep {
    boolean execute();
    boolean compensate();
    String getName();
}

The execute() method runs the forward progress logic, returning a boolean indicating success. compensate() runs the rollback logic to reverse the execution if a subsequent step fails.


Concrete Service Steps (Commands)

We implement three mock steps to simulate distributed services: OrderCreationStep, PaymentProcessingStep, and InventoryReservationStep.


public class OrderCreationStep implements SagaStep {
    @Override
    public boolean execute() {
        System.out.println("[Order Service] Creating temporary order... SUCCESS");
        return true;
    }

    @Override
    public boolean compensate() {
        System.out.println("[Order Service] Deleting order records... ROLLBACK SUCCESS");
        return true;
    }

    @Override
    public String getName() { return "Order Creation"; }
}

The OrderCreationStep represents the initial step in the transaction. Its compensation deletes the temporary order.


public class PaymentProcessingStep implements SagaStep {
    private final boolean shouldSucceed;

    public PaymentProcessingStep(boolean shouldSucceed) {
        this.shouldSucceed = shouldSucceed;
    }

    @Override
    public boolean execute() {
        if (shouldSucceed) {
            System.out.println("[Payment Service] Charging customer credit card... SUCCESS");
            return true;
        } else {
            System.out.println("[Payment Service] Charging customer credit card... FAILED");
            return false;
        }
    }

    @Override
    public boolean compensate() {
        System.out.println("[Payment Service] Refunding charge to customer... ROLLBACK SUCCESS");
        return true;
    }

    @Override
    public String getName() { return "Payment Processing"; }
}

The PaymentProcessingStep constructor allows simulating authorization failures. Its compensation refunds the customer.


public class InventoryReservationStep implements SagaStep {
    private final boolean shouldSucceed;

    public InventoryReservationStep(boolean shouldSucceed) {
        this.shouldSucceed = shouldSucceed;
    }

    @Override
    public boolean execute() {
        if (shouldSucceed) {
            System.out.println("[Inventory Service] Blocking item stock... SUCCESS");
            return true;
        } else {
            System.out.println("[Inventory Service] Blocking item stock... FAILED");
            return false;
        }
    }

    @Override
    public boolean compensate() {
        System.out.println("[Inventory Service] Releasing item stock block... ROLLBACK SUCCESS");
        return true;
    }

    @Override
    public String getName() { return "Inventory Reservation"; }
}

The InventoryReservationStep blocks stock in the forward path, and releases the stock block as its compensation.


Saga Class

The Saga class acts as the data model container holding the sequence of steps, tracking progress, and updating the transaction status.


import java.util.ArrayList;
import java.util.List;

public class Saga {
    private final List<SagaStep> steps;
    private SagaStatus status;
    private int currentStepIndex;

    public Saga() {
        this.steps = new ArrayList<>();
        this.status = SagaStatus.IN_PROGRESS;
        this.currentStepIndex = 0;
    }

    public void addStep(SagaStep step) {
        steps.add(step);
    }

    public List<SagaStep> getSteps() { return steps; }
    
    public synchronized SagaStatus getStatus() { return status; }
    public synchronized void setStatus(SagaStatus status) { this.status = status; }

    public synchronized int getCurrentStepIndex() { return currentStepIndex; }
    public synchronized void setCurrentStepIndex(int index) { this.currentStepIndex = index; }
}

Getter and setter methods are synchronized to ensure thread safety when the orchestrator evaluates or updates status metrics.


SagaOrchestrator Class

The SagaOrchestrator contains the workflow execution engine. It loops forward through steps, and triggers compensations in reverse order if a step fails.


public class SagaOrchestrator {

    public boolean execute(Saga saga) {
        System.out.println("Starting Saga Transaction Workflow execution...");
        saga.setStatus(SagaStatus.IN_PROGRESS);

        for (int i = 0; i < saga.getSteps().size(); i++) {
            SagaStep step = saga.getSteps().get(i);
            saga.setCurrentStepIndex(i);

            System.out.println("Executing step: " + step.getName());
            boolean success = step.execute();

            if (!success) {
                System.out.println("Step " + step.getName() + " failed. Triggering rollback...");
                rollback(saga);
                return false;
            }
        }

        saga.setStatus(SagaStatus.SUCCESSFUL);
        System.out.println("Saga Transaction Workflow completed successfully.");
        return true;
    }

    private void rollback(Saga saga) {
        saga.setStatus(SagaStatus.COMPENSATING);
        
        // Iterate backwards from the failed step index
        int failedIndex = saga.getCurrentStepIndex();
        for (int i = failedIndex - 1; i >= 0; i--) {
            SagaStep step = saga.getSteps().get(i);
            System.out.println("Compensating step: " + step.getName());
            
            boolean compensationSuccess = false;
            int retries = 3;

            // Retry loop for compensation errors
            while (!compensationSuccess && retries > 0) {
                compensationSuccess = step.compensate();
                if (!compensationSuccess) {
                    retries--;
                    System.err.println("Compensate failed for " + step.getName() + ". Retrying... (" + retries + " left)");
                }
            }

            if (!compensationSuccess) {
                System.err.println("CRITICAL: Compensation failed for " + step.getName() + " after retries. Alert triggered.");
            }
        }

        saga.setStatus(SagaStatus.FAILED);
        System.out.println("Saga Transaction Workflow rollback completed.");
    }
}

Here is an explanation of the core operations in the SagaOrchestrator class:

  • execute() sets the status to IN_PROGRESS, executes steps sequentially, and updates the step index. If a step returns false, it immediately calls rollback().
  • rollback() transitions the status to COMPENSATING, loops backward from the failed step index, and executes compensations. It includes a retry loop to handle compensation errors and logs alerts if retries fail.

Main Driver Class

This class tests our Saga orchestrator by simulating two scenarios: a successful transaction, and a payment failure that triggers a rollback.


public class Main {
    public static void main(String[] args) {
        SagaOrchestrator orchestrator = new SagaOrchestrator();

        System.out.println("==========================================");
        System.out.println("Scenario 1: Testing Successful Saga");
        System.out.println("==========================================");
        
        Saga successSaga = new Saga();
        successSaga.addStep(new OrderCreationStep());
        successSaga.addStep(new PaymentProcessingStep(true)); // simulate success
        successSaga.addStep(new InventoryReservationStep(true)); // simulate success

        orchestrator.execute(successSaga);
        System.out.println("Saga Status: " + successSaga.getStatus());

        System.out.println("\n==========================================");
        System.out.println("Scenario 2: Testing Failed Saga (Payment Fails)");
        System.out.println("==========================================");

        Saga failedSaga = new Saga();
        failedSaga.addStep(new OrderCreationStep());
        failedSaga.addStep(new PaymentProcessingStep(false)); // simulate payment failure
        failedSaga.addStep(new InventoryReservationStep(true));

        orchestrator.execute(failedSaga);
        System.out.println("Saga Status: " + failedSaga.getStatus());
    }
}

The main() driver configures workflows, executes both scenarios, and outputs transaction steps and rollback operations to verify the orchestrator's behavior.


Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...