From 25d12fdc06d5299b6deca2b9d9a7664fc396efb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilkka=20Sepp=C3=A4l=C3=A4?= Date: Sun, 19 May 2024 16:04:44 +0300 Subject: [PATCH] docs: Saga explanation (#2962) * docs: update cqrs pattern name * docs: add saga docs --- cqrs/README.md | 6 +- saga/README.md | 150 ++++++++++++++---- .../com/iluwatar/saga/choreography/Saga.java | 28 +--- .../choreography/WithdrawMoneyService.java | 2 +- .../saga/orchestration/ChapterResult.java | 7 +- .../com/iluwatar/saga/orchestration/Saga.java | 12 +- .../orchestration/WithdrawMoneyService.java | 2 +- 7 files changed, 142 insertions(+), 65 deletions(-) diff --git a/cqrs/README.md b/cqrs/README.md index cd389cef8..4858e87ec 100644 --- a/cqrs/README.md +++ b/cqrs/README.md @@ -1,5 +1,5 @@ --- -title: CQRS +title: Command Query Responsibility Segregation category: Architectural language: en tag: @@ -8,6 +8,10 @@ tag: - Scalability --- +## Also known as + +* CQRS + ## Intent Command Query Responsibility Segregation (CQRS) aims to segregate the operations that modify the state of an application (commands) from the operations that read the state (queries). This separation allows for more flexible and optimized designs, especially in complex systems. diff --git a/saga/README.md b/saga/README.md index f25e10114..2963cd971 100644 --- a/saga/README.md +++ b/saga/README.md @@ -1,47 +1,141 @@ --- title: Saga -category: Concurrency +category: Resilience language: en tag: - - Cloud distributed + - Asynchronous + - Decoupling + - Fault tolerance + - Integration + - Microservices + - Transactions --- -## Also known as -This pattern has a similar goal with two-phase commit (XA transaction) - ## Intent -This pattern is used in distributed services to perform a group of operations atomically. -This is an analog of transaction in a database but in terms of microservices architecture this is executed -in a distributed environment + +To manage and coordinate distributed transactions across multiple services in a fault-tolerant and reliable manner. ## Explanation -A saga is a sequence of local transactions in a certain context. If one transaction fails for some reason, -the saga executes compensating transactions(rollbacks) to undo the impact of the preceding transactions. -There are two types of Saga: -- Choreography-Based Saga. -In this approach, there is no central orchestrator. -Each service participating in the Saga performs their transaction and publish events. -The other services act upon those events and perform their transactions. -Also, they may or not publish other events based on the situation. +Real-world example -- Orchestration-Based Saga -In this approach, there is a Saga orchestrator that manages all the transactions and directs -the participant services to execute local transactions based on events. -This orchestrator can also be though of as a Saga Manager. +> 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 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** + +The Saga design pattern is a sequence of local transactions where each transaction updates data within a single service. The Saga pattern is a way to manage transactions and it's particularly useful in microservices architecture where each service has its own database. + +The Saga pattern is implemented in the `Saga` class. 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. + +```java +public class Saga { + + private final List chapters; + + private Saga() { + this.chapters = new ArrayList<>(); + } + + public Saga chapter(String name) { + this.chapters.add(new Chapter(name)); + return this; + } + + public Chapter get(int idx) { + return chapters.get(idx); + } + + public boolean isPresent(int idx) { + return idx >= 0 && idx < chapters.size(); + } + + public static Saga create() { + return new Saga(); + } + + public static class Chapter { + String name; + + public Chapter(String name) { + this.name = name; + } + } +} +``` + +**Explanation:** + +The `WithdrawMoneyService` class represents a service that performs a local transaction. It extends the `Service` class and overrides the `process` method to perform the transaction. If the transaction fails, it sets the status of the saga to `ROLLBACK`. + +```java +public class WithdrawMoneyService extends Service { + @Override + public String getName() { + return "withdrawing Money"; + } + + @Override + public ChapterResult process(String value) { + if (value.equals("bad_order") || value.equals("crashed_order")) { + LOGGER.info("The chapter '{}' has been started. But the exception has been raised." + + "The rollback is about to start", + getName()); + return ChapterResult.failure(value); + } + return super.process(value); + } +} +``` + +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. ## Class diagram -![alt text](./etc/saga.urm.png "Saga pattern class diagram") + +![Saga](./etc/saga.urm.png "Saga pattern class diagram") ## Applicability -Use the Saga pattern, if: -- you need to perform a group of operations related to different microservices atomically -- you need to rollback changes in different places in case of failure one of the operation -- you need to take care of data consistency in different places including different databases -- you can not use 2PC(two phase commit) +* 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. + +## Known Uses + +* 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. + +## Consequences + +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 Patterns + +* [Event Sourcing](https://java-design-patterns.com/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)](https://java-design-patterns.com/patterns/cqrs/): Can be used in conjunction with the Saga pattern to separate command and query responsibilities, improving scalability and fault tolerance. ## Credits -- [Pattern: Saga](https://microservices.io/patterns/data/saga.html) -- [Saga distributed transactions pattern](https://docs.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga) +* [Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems](https://amzn.to/3y6yv1z) +* [Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions](https://amzn.to/3WcFVui) +* [Microservices Patterns: With examples in Java](https://amzn.to/3UyWD5O) +* [Saga pattern - microservices.io](https://microservices.io/patterns/data/saga.html) +* [Saga distributed transactions pattern - Microsoft](https://docs.microsoft.com/en-us/azure/architecture/reference-architectures/saga/saga) diff --git a/saga/src/main/java/com/iluwatar/saga/choreography/Saga.java b/saga/src/main/java/com/iluwatar/saga/choreography/Saga.java index a98abd332..c04a63dfd 100644 --- a/saga/src/main/java/com/iluwatar/saga/choreography/Saga.java +++ b/saga/src/main/java/com/iluwatar/saga/choreography/Saga.java @@ -27,6 +27,8 @@ package com.iluwatar.saga.choreography; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import lombok.Getter; +import lombok.Setter; /** * Saga representation. Saga consists of chapters. Every ChoreographyChapter is executed a certain @@ -154,37 +156,19 @@ public class Saga { * outcoming parameter). */ public static class Chapter { + @Getter private final String name; + @Setter private ChapterResult result; + @Getter + @Setter private Object inValue; - public Chapter(String name) { this.name = name; this.result = ChapterResult.INIT; } - public Object getInValue() { - return inValue; - } - - public void setInValue(Object object) { - this.inValue = object; - } - - public String getName() { - return name; - } - - /** - * set result. - * - * @param result {@link ChapterResult} - */ - public void setResult(ChapterResult result) { - this.result = result; - } - /** * the result for chapter is good. * diff --git a/saga/src/main/java/com/iluwatar/saga/choreography/WithdrawMoneyService.java b/saga/src/main/java/com/iluwatar/saga/choreography/WithdrawMoneyService.java index 8dccab9d2..c9b370898 100644 --- a/saga/src/main/java/com/iluwatar/saga/choreography/WithdrawMoneyService.java +++ b/saga/src/main/java/com/iluwatar/saga/choreography/WithdrawMoneyService.java @@ -45,7 +45,7 @@ public class WithdrawMoneyService extends Service { if (inValue.equals("bad_order")) { LOGGER.info("The chapter '{}' has been started. But the exception has been raised." + "The rollback is about to start", - getName(), inValue); + getName()); saga.setCurrentStatus(Saga.ChapterResult.ROLLBACK); return saga; } diff --git a/saga/src/main/java/com/iluwatar/saga/orchestration/ChapterResult.java b/saga/src/main/java/com/iluwatar/saga/orchestration/ChapterResult.java index 977680112..a348b3a72 100644 --- a/saga/src/main/java/com/iluwatar/saga/orchestration/ChapterResult.java +++ b/saga/src/main/java/com/iluwatar/saga/orchestration/ChapterResult.java @@ -24,19 +24,18 @@ */ package com.iluwatar.saga.orchestration; +import lombok.Getter; + /** * Executing result for chapter. * * @param incoming value */ public class ChapterResult { + @Getter private final K value; private final State state; - public K getValue() { - return value; - } - ChapterResult(K value, State state) { this.value = value; this.state = state; diff --git a/saga/src/main/java/com/iluwatar/saga/orchestration/Saga.java b/saga/src/main/java/com/iluwatar/saga/orchestration/Saga.java index 5c3756725..9057a23e3 100644 --- a/saga/src/main/java/com/iluwatar/saga/orchestration/Saga.java +++ b/saga/src/main/java/com/iluwatar/saga/orchestration/Saga.java @@ -26,6 +26,8 @@ package com.iluwatar.saga.orchestration; import java.util.ArrayList; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; /** * Saga representation. Saga consists of chapters. Every ChoreographyChapter is executed by a @@ -70,15 +72,9 @@ public class Saga { /** * class represents chapter name. */ + @AllArgsConstructor + @Getter public static class Chapter { String name; - - public Chapter(String name) { - this.name = name; - } - - public String getName() { - return name; - } } } diff --git a/saga/src/main/java/com/iluwatar/saga/orchestration/WithdrawMoneyService.java b/saga/src/main/java/com/iluwatar/saga/orchestration/WithdrawMoneyService.java index bea7c3a33..fe443cbd1 100644 --- a/saga/src/main/java/com/iluwatar/saga/orchestration/WithdrawMoneyService.java +++ b/saga/src/main/java/com/iluwatar/saga/orchestration/WithdrawMoneyService.java @@ -38,7 +38,7 @@ public class WithdrawMoneyService extends Service { if (value.equals("bad_order") || value.equals("crashed_order")) { LOGGER.info("The chapter '{}' has been started. But the exception has been raised." + "The rollback is about to start", - getName(), value); + getName()); return ChapterResult.failure(value); } return super.process(value);