Create your own
Lesson illustration

Orchestrating Sagas with Centralized Command and Compensation

Welcome back! In our previous lesson, we implemented a choreographed saga where services communicated through a decentralized web of events. This approach gives us great flexibility and loose coupling.

Today, we'll explore the other side of the coin. What if you need more control, clearer visibility into the business process, and a single place to manage the logic? This is where the orchestration pattern comes in. Your learning outcome for this lesson is to implement an orchestrated saga using a central service to manage command and compensation flows.

We will build a system where a dedicated orchestrator acts as a conductor, telling each service what to do and when. This will provide a strong contrast to the choreography pattern and equip you to discuss the trade-offs between both approaches—a very common topic in senior-level microservice interviews.

Let's start by visualizing the difference.

Saga Pattern Cheatsheet: Orchestration vs. Choreography
This cheatsheet provides a side-by-side comparison of the two saga patterns. The top half shows orchestration, with a central orchestrator sending commands. The bottom half, which we implemented last time, shows choreography, where services react to each other's events.

As you can see, the core idea of orchestration is centralization. Let's dive into what that means in practice.

1. The Orchestration Pattern: A Central Conductor

In an orchestrated saga, one service is designated as the orchestrator. This service is responsible for the entire lifecycle of a business transaction. It doesn't contain the business logic of the participant services (like payment processing or inventory management), but it knows the sequence of operations.

The orchestrator's job is to:

  1. Send a command to a service to execute a local transaction.
  2. Receive the outcome (success or failure).
  3. Based on the outcome, either send a command to the next service in the sequence or trigger compensating transactions for all previously completed steps.
Saga Design Pattern with Orchestration
This flowchart shows a central "Orchestration controller" managing a travel booking saga. It issues `RUN` commands to the Airline, Hotel, and Car Rental services. If any step fails, the controller explicitly issues `Compensation` commands to roll back the previous steps.

For this to work, the participant services must be designed differently than in a choreographed system:

  • Instead of listening for events, they expose command-based APIs.
  • Crucially, for every action (e.g., debit), they must also expose a corresponding compensating action (e.g., credit).

Let's look at a more detailed sequence diagram to understand the message flow for both a happy path and a failure scenario.

Microservices Java/Spring-Boot Saga Orchestration

This GitHub repository for a hotel reservation system contains excellent sequence diagrams that illustrate the orchestrator's role. We'll focus on the diagrams, not the specific underlying tech (like Debezium) for now.

In the "Implementation" section, study the two sequence diagrams: "Happy Path" and "Unhappy Path". Pay close attention to how the ReservationService (acting as the orchestrator) sends commands (book-room, process-payment) and receives replies. Notice how in the unhappy path, it explicitly sends a cancel-booking command upon payment failure.

Having seen the flow, let's now implement it.

2. Implementing an Orchestrated Saga with Spring Boot

We will build an e-commerce saga with an OrderOrchestrator service that coordinates with a PaymentService and an InventoryService. We'll use the following article as our guide, as it provides a very clean and extensible implementation pattern.

Orchestration Saga Pattern With Spring Boot

First, let's look at how the participant services are designed. The article "Orchestration Saga Pattern With Spring Boot" by Vinsguru provides a clear example. We'll examine the InventoryService and PaymentService.

Read the sections "Inventory Service" and "Payment Service". Notice that each service exposes two distinct endpoints: one for the main action (/deduct, /debit) and one for the compensating action (/add, /credit). This API design is fundamental to the orchestration pattern.

With the participant services ready, the main work lies in building the orchestrator. A robust way to do this is by modeling the saga as a workflow composed of distinct steps.

Defining the Workflow Abstraction

A great design pattern for an orchestrator is to define interfaces for a Workflow and a WorkflowStep. Each step in the saga will be an implementation of WorkflowStep, responsible for its own processing and compensation logic.

public interface WorkflowStep {
    WorkflowStepStatus getStatus();
    Mono<Boolean> process(); // The command
    Mono<Boolean> revert();  // The compensation
}

public interface Workflow {
    List<WorkflowStep> getSteps();
}

Note: The code uses Project Reactor (Mono) for non-blocking communication, which is ideal for I/O-bound tasks like calling other services. Given your goal of working with production-ready systems, this is a pattern you should be very familiar with.

Implementing the Orchestrator Logic

Now, let's see how these abstractions are used to build the complete orchestrator.

Orchestration Saga Pattern With Spring Boot

This next section is the core of our lesson. It details the full implementation of the orchestrator, from the individual workflow steps to the service that runs them.

Read the entire "Order Orchestrator" section carefully. Pay attention to: InventoryStep and PaymentStep: How they implement the process() and revert() methods using WebClient to call the participant services. OrchestratorService: How the orderProduct method chains the steps together using Flux.flatMap(WorkflowStep::process). Error Handling: The critical use of .onErrorResume(ex -> this.revertOrder(workflow, requestDTO)). This is how the entire compensation flow is triggered. Compensation Logic: The revertOrder method, which filters for completed steps and calls their revert() method.

This implementation elegantly captures the essence of orchestration. The OrchestratorService has a complete, centralized view of the workflow. The logic for the happy path (process) and the failure path (revert) is clearly separated within each step but executed from a single control point.

Test your understanding!

Imagine you need to add a ShippingService to this saga. It should be called after the inventory is successfully deducted. The ShippingService exposes two endpoints: /schedule (for the action) and /cancel (for the compensation).

How would you modify the OrderOrchestrator to include this new step? Describe the classes you would create or modify.

Show answer

You would follow the established pattern:

  1. Create a ShippingStep class:

    • It would implement the WorkflowStep interface.
    • Its constructor would take a WebClient configured for the ShippingService and a ShippingRequestDTO.
    • The process() method would make a POST call to the /schedule endpoint.
    • The revert() method would make a POST call to the /cancel endpoint.
  2. Modify OrchestratorService:

    • In the getOrderWorkflow method, you would instantiate your new ShippingStep.
    • You would add this shippingStep instance to the list of steps when creating the OrderWorkflow object. The new order would be List.of(paymentStep, inventoryStep, shippingStep).

The OrchestratorService's core processing and revert logic (orderProduct and revertOrder) would not need to change at all, because it's designed to work with any list of WorkflowSteps. This demonstrates the power and extensibility of this design.

3. Orchestration vs. Choreography: The Trade-offs

Now that you've seen both patterns implemented, you're in a strong position to discuss their trade-offs in an interview.

AspectOrchestrationChoreography (from previous lesson)
CouplingServices are coupled to the orchestrator's API/commands, but not to each other.Services are loosely coupled, only needing to know about events, not other services.
ComplexityBusiness logic is centralized and simpler to understand. Participant services are simple. The orchestrator can become complex (a "god object").Logic is distributed, which can be hard to trace. Participant services are more complex as they must handle event consumption.
ObservabilityEasy to monitor. The state of the transaction is known by the orchestrator.Harder to monitor. You need distributed tracing to see the end-to-end flow across multiple event topics.
Point of FailureThe orchestrator is a single point of failure and a potential performance bottleneck.No single point of failure, making the system more resilient.
ExtensibilityAdding a new step requires modifying the central orchestrator.Adding a new service that just listens to an existing event doesn't require any changes to existing services.

The choice between them depends on the context. Orchestration is often preferred for complex, long-running processes where visibility and control are paramount. Choreography shines in systems that prioritize scalability, resilience, and enabling teams to work independently.

Conclusion

In this lesson, we demystified the orchestration saga pattern by implementing a central orchestrator that manages command and compensation flows. This approach gives you explicit control over distributed transactions, making them easier to understand and monitor at the cost of tighter coupling and a potential single point of failure.

Key Takeaways:

  • Central Control: The orchestrator owns the business workflow and directs participant services via commands.
  • Explicit Compensation: If a step fails, the orchestrator is responsible for explicitly calling the compensation endpoints of prior successful steps.
  • Clean Abstraction: Using Workflow and WorkflowStep interfaces creates a clean, testable, and extensible design for the orchestrator.
  • Clear Trade-offs: You can now confidently compare orchestration with choreography, discussing the pros and cons of each in terms of coupling, complexity, and observability.

Next Up

So far in this module, we've focused on ensuring data consistency when writing data across multiple services. But what about reading it? If a user wants to see a complete order with payment, inventory, and shipping details, querying all those services on-the-fly can be slow and complex.

In our next lesson, we will tackle this problem by exploring the Command Query Responsibility Segregation (CQRS) pattern. You will learn how to create optimized read models (queries) that are separate from the models used for writing data (commands), a powerful technique for building high-performance microservices.

Can't find a good explanation? Sign up and we'll make it for you

Sign up