Saga Pattern in Java: Mastering Long-Running Transactions in Distributed Systems
Intent of Saga Design Pattern
To manage and coordinate distributed transactions across multiple services in a fault-tolerant and reliable manner.
Detailed Explanation of Saga Pattern with Real-World Examples
Real-world example
Imagine a travel agency coordinating a vacation package for a customer. The package includes booking a flight, reserving a hotel room, and renting a car. Each of these bookings is managed by a different service provider. If the flight booking is successful but the hotel is fully booked, the agency needs to cancel the flight and notify the customer. This ensures that the customer does not end up with only a partial vacation package. In the Saga pattern, this scenario is managed by a series of coordinated transactions, with compensating actions (like canceling the flight) to maintain consistency.
In plain words
The Saga pattern in Java coordinates distributed transactions across microservices using a sequence of events and compensating actions to ensure data consistency and fault tolerance.
Wikipedia says
Long-running transactions (also known as the saga interaction pattern) are computer database transactions that avoid locks on non-local resources, use compensation to handle failures, potentially aggregate smaller ACID transactions (also referred to as atomic transactions), and typically use a coordinator to complete or abort the transaction. In contrast to rollback in ACID transactions, compensation restores the original state, or an equivalent, and is business-specific. For example, the compensating action for making a hotel reservation is canceling that reservation.
Programmatic Example of Saga Pattern in Java
The Saga design pattern is a sequence of local transactions where each transaction updates data within a single service. It's particularly useful in a microservices architecture where each service has its own database. The Saga pattern ensures data consistency and fault tolerance across services. Here are the key components of the Saga pattern:
Saga: A Saga is a sequence of local transactions, each of which is called a chapter. The Saga manages the sequence of these transactions, ensuring that each transaction is performed in the correct order and that the Saga is rolled back if a transaction fails.
Chapter: Each chapter in a Saga represents a local transaction. A chapter has a name, a result (which can be
INIT
,SUCCESS
, orROLLBACK
), and an input value. TheChapter
class provides methods to get and set these properties.Service: A service performs a local transaction. It processes the input value of a chapter and returns a
ChapterResult
. If the transaction fails, it sets the status of the chapter toROLLBACK
.Service Discovery: This component is responsible for discovering available services and executing the Saga. It processes each chapter in the Saga in order. If a chapter fails, the Saga will be rolled back.
Saga Result: The result of a Saga can be
PROGRESS
,FINISHED
, orROLLBACKED
. This is determined by thegetResult
method of theSaga
class.
In a real-world application, the Service
class would contain the logic to perform the local transaction and handle failures. The Saga
class would manage the sequence of local transactions, ensuring that each transaction is performed in the correct order and that the Saga is rolled back if a transaction fails.
Snippet 1: Creating a Saga
The first step in using the Saga pattern is to create a Saga. A Saga is a sequence of chapters, each representing a local transaction. The Saga
class provides methods to add chapters and to check if a chapter is present.
// Create a new Saga
Saga saga = Saga.create();
Snippet 2: Adding Chapters to the Saga
Each chapter in a Saga represents a local transaction. We can add chapters to the Saga using the chapter
method.
// Add chapters to the Saga
saga.chapter("init an order");
saga.chapter("booking a Fly");
saga.chapter("booking a Hotel");
saga.chapter("withdrawing Money");
Snippet 3: Setting Input Values for Chapters
Each chapter in a Saga can have an input value. We can set the input value for the last added chapter using the setInValue
method.
// Set input values for the chapters
saga.chapter("init an order").setInValue("good_order");
Snippet 4: Executing the Saga
We can execute the Saga using a service. The service will process each chapter in the Saga in order. If a chapter fails, the Saga will be rolled back.
// Execute the Saga
var service = sd.findAny();
var goodOrderSaga = service.execute(saga);
Snippet 5: Checking the Result of the Saga
We can check the result of the Saga using the getResult
method. This method returns the result of the Saga, which can be PROGRESS
, FINISHED
, or ROLLBACKED
.
// Check the result of the Saga
SagaResult result = goodOrderSaga.getResult();
The SagaApplication
class has a main
method for running the example.
@Slf4j
public class SagaApplication {
public static void main(String[] args) {
var sd = serviceDiscovery();
var service = sd.findAny();
var goodOrderSaga = service.execute(newSaga("good_order"));
var badOrderSaga = service.execute(newSaga("bad_order"));
LOGGER.info("orders: goodOrder is {}, badOrder is {}",
goodOrderSaga.getResult(), badOrderSaga.getResult());
}
private static Saga newSaga(Object value) {
return Saga
.create()
.chapter("init an order").setInValue(value)
.chapter("booking a Fly")
.chapter("booking a Hotel")
.chapter("withdrawing Money");
}
private static ServiceDiscoveryService serviceDiscovery() {
var sd = new ServiceDiscoveryService();
return sd
.discover(new OrderService(sd))
.discover(new FlyBookingService(sd))
.discover(new HotelBookingService(sd))
.discover(new WithdrawMoneyService(sd));
}
}
- Saga: The
SagaApplication
creates a new Saga using theSaga.create()
method. It then adds chapters to the Saga using thechapter
method and sets the input value for each chapter using thesetInValue
method. - Service: The
SagaApplication
uses services to execute the chapters in the Saga. Each service represents a local transaction. TheSagaApplication
uses theServiceDiscoveryService
to discover available services and execute the Saga. - Service Discovery: The
ServiceDiscoveryService
is used to discover available services. TheSagaApplication
uses this to find a service and execute the Saga. - Saga Execution: The
SagaApplication
executes the Saga using theexecute
method of a service. It creates two Sagas, one for a good order and one for a bad order, and executes them. - Saga Result: The
SagaApplication
checks the result of the Saga using thegetResult
method. It logs the result of the good order Saga and the bad order Saga.
In summary, the SagaApplication
creates a Saga, adds chapters to it, sets the input value for each chapter, discovers services, executes the Saga using a service, and checks the result of the Saga.
Running the example produces the following console output:
11:32:17.779 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'init an order' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Fly' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Hotel' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'withdrawing Money' has been started. The data good_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- the saga has been finished with FINISHED status
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'init an order' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Fly' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'booking a Hotel' has been started. The data bad_order has been stored or calculated successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The chapter 'withdrawing Money' has been started. But the exception has been raised.The rollback is about to start
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'booking a Hotel' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'booking a Fly' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- The Rollback for a chapter 'init an order' has been started. The data bad_order has been rollbacked successfully
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.Service -- the saga has been finished with ROLLBACKED status
11:32:17.782 [main] INFO com.iluwatar.saga.choreography.SagaApplication -- orders: goodOrder is FINISHED, badOrder is ROLLBACKED
This is a basic example of how to use the Saga design pattern. In a real-world application, the Saga
class would manage the sequence of local transactions, ensuring that each transaction is performed in the correct order and that the Saga is rolled back if a transaction fails.
When to Use the Saga Pattern in Java
- When you have a complex transaction that spans multiple microservices.
- When you need to ensure data consistency across services without using a traditional two-phase commit.
- When you need to handle long-running transactions in an asynchronous manner.
Real-World Applications of Saga Pattern in Java
- E-commerce platforms managing orders, inventory, and payment services.
- Banking systems coordinating between account debits and credits across multiple services.
- Travel booking systems coordinating flights, hotels, and car rentals.
Benefits and Trade-offs of Saga Pattern
Benefits:
- Improved fault tolerance and reliability.
- Scalability due to decoupled services.
- Flexibility in handling long-running transactions.
Trade-offs:
- Increased complexity in handling compensating transactions.
- Requires careful design to handle partial failures and rollback scenarios.
- Potential latency due to asynchronous nature.
Related Java Design Patterns
- Event Sourcing: Used to capture state changes as a sequence of events, which can complement the Saga pattern by providing a history of state changes.
- Command Query Responsibility Segregation (CQRS): Can be used in conjunction with the Saga pattern to separate command and query responsibilities, improving scalability and fault tolerance.
References and Credits
- Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems
- Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions
- Microservices Patterns: With examples in Java
- Pattern: Saga (microservices.io)
- Saga distributed transactions pattern (Microsoft)