Skip to main content

Use the SAGA Pattern

🎯context

You want to design transaction-like behaviour using the SAGA pattern support in your Java Domain Service based project.

Description​

This How-To will explain how to design transaction-like behaviour using the Saga pattern support in your Java Domain Service based project based on a given example involving four services.

ℹ️note

This How-To covers the implementation of a SAGA orchestrator in Java Domain Service based microservice. To keep things simple the SAGA participants are kept deliberately simple. If the business logic that is called by the orchestrator gets more involved, more complex patterns like nested SAGAs might be required.

Preconditions​

  • You have a Narayana LRA coordinator running in your cluster.

Overview​

The setup and flow of communication for this example scenario is given in the following diagram:

Services Overview diagram

Example use case​

We want to enable our new customers to register themselves via a website. After successful registration, the customer shall be available in the Customer Service. Also, the customers credit card should be debited with a registration fee. When one of the steps fails the entire registration shall be undone.

Even if it is a very simple example (and using a SAGA could be easily avoided), it shall still be used to explain how to apply the SAGA pattern and leaving unnecessary complexity out.

In this case, the SAGA then consists of two participants, the customer creation and the execution of the credit card payment.

Besides the LRA Coordinator, the Registration Service, the Payment Service and the Customer Service are required for this use case. The LRA Coordinator communicates with the Registration Service, which orchestrates the entire Saga transaction.

Modelling and implementing the Registration Service​

The Registration Service contains two domain namespaces, the Customer Registration and the SAGA namespace. While the first namespace holds the registration information itself (e.g., customer name, credit card details etc.), the SAGA namespace contains the design artifacts for the SAGA.

Whenever a client initiates a new Customer Registration, the relevant information will be persisted first. After that the Saga aggregate is created and stored it in its database, along with the current status of the overall transaction, as well as the Participants and their individual status.

❗️info

To use the SAGA support, your service needs to be a Domain Service with Java Spring Boot Stack 2.0 where the SAGA support is enabled (see screenshot below).

Saga support enabled

Modelling the SAGA Domain Namespace​

The Domain Model of the SAGA namespace contains the SAGA aggregate, which consists of only a few Entities to track the status of processing the SAGA and its Participants.

Below, a simplified domain model of the Customer Self Registration Service is shown.

Customer self registration

Designing the domain namespace follows the same steps as it is for any other Domain Service. Therefore, only the specifics of the SAGA domain will be mentioned.

Customer Registration Saga (Root entity)​
  • References the Customer Registration Requests (which contains all for the registration relevant customer and credit card information)

  • lraId (id of the “long running action” – which the LRA coordinator will provide)

  • List of Participants

  • References to the Saga Status Entity (current status, which can either be IN_PROGRESS, COMPLETED or FAILED, as well as the history of previous status)

Participant (abstract Entity)​
  • References to the Participant Status entity (current status, which can either be PREPARED, IN_PROGRESS, WITHDRAWN, FAILED, COMPLETED or COMPENSATED as well as the history of status)

The Customer Participant and the Payment Participant hold only few additional information which is needed to process or track the specific task, in case of the Customer Participant the customerId, which is the result of a successful customer creation and necessary in case of compensation.

đź’ˇtip

The customer and credit card details needed to perform the SAGA are not stored in the SAGA aggregate; instead the Customer Registration Request aggregate is referenced.

ℹ️note

The Domain Model shown in this How-To was designed to cover the example’s requirements. For your concrete business requirements, a different design may be useful.

Create Domain Services for the SAGA​

To perform the SAGA, it is also necessary to create several Domain Services which take over the Orchestration of the SAGA or perform the Participants’ logic. Therefore, these Saga Pattern Roles can be chosen when creating a Domain Service.

Create Participant Service​

When creating a Domain Service for the SAGA, additional configuration options can be chosen, see Modelling SAGA services.

The in this example as Participants used Domain Services are configured as follows:

Saga Pattern Details

Create Orchestrator Service​

The Orchestrator Service almost has the same configuration options as a Participant service. Additionally, you need to add the Participant Services which shall be orchestrated.

Orchestrator Service

To implement the Customer Self Registration the following Domain Services are necessary:

  • Saga Orchestrator

  • Payment Participant

  • Customer (Creation) Participant

While the Orchestrator is taking care of coordinating the different Participants and the entire SAGA, the Participant Domain Service performs the individual business logic, e.g. processing a credit card payment. Therefore, the service’s description must contain the initial logic as well as the logic to be performed in case of compensation or completion.

❗️info

All Domain Services having a SAGA role need to have the same input.

đź’ˇtip

In case of compensation, all onCompensate methods are called in parallel, no matter whether it is the participant which has failed, the participant has already been completed before or was not yet executed. This means that the business logic must be idempotent and consider the current state of the Participant.

Depending on the current status of the Participant different actions need to be taken when the onCompensate method was called:

  • COMPENSATED - Nothing to do (already compensated)

  • FAILED - Nothing to do (it has already failed)

  • WITHDRAWN - Nothing to do (not yet performed)

  • PREPARED - Status need to get changed to WITHDRAWN

  • COMPLETED - Perform compensation logic, then change status to COMPENSATED

  • IN_PROGRESS - Need to consider the current state of progress

Update SAGA Status​

Depending on the status of all Participants, the status of the SAGA needs to be updated:

  • Participants either WITHDRAWN, FAILED or COMPENSATED -> SAGA status FAILED

  • Participants all COMPLETED - > SAGA status COMPLETED

ℹ️note

Both the onCompensate and onComplete methods are called in parallel (both for the Orchestrator and for the Participant Domain Services). As the SAGA status depends on the status of its participants, we did not use the onCompensate method of the Orchestrator to update the SAGA status.

đź’ˇtip

To solve this, an Event needs to be published whenever the status of a Participant got changed. The payload of the Event contains the following properties:

  • lraId

  • newStatus

  • previousStatus

  • customerId *

Receiving the Event, an Agent needs to retrieve the SAGA by the lraId and checks whether all of its Participants have the required status to be able to set the final SAGA status (as mentioned above).

Having now the SAGA domain modelled, the logic needs to be implemented.

Implementation of the SAGA Logic​

Each participant is represented by a class which comprises three methods (depending on the configuration of the particular Domain Service) that will be triggered by the LRA Coordinator at the appropriate time:

  • execute

  • onComplete

  • onCompensate

In this example, there are two participants, Payment Participant and Customer Participant. Each one is responsible for executing a defined business logic within the Saga. The Payment Participant will trigger a payment in the Payment Service, whereas the Customer Service will trigger the creation of a new customer in the Customer Service. Both of these services will subsequently store an entity of the specific domain in their databases.

The execute() method of each participant contains the implementation to perform this business logic.

The Orchestrator Configurator Service contains a configure() method to define the Saga’s route. Here it is possible to determine whether or not the participants should be executed in parallel or in a given order, which methods to call on completion or compensation of all participants, and also if any methods should be called before and after the Saga. Here is the configure() method used for this How-To:

@Override
public void configure() throws Exception {
String routeId = "route_so_" + StringUtils.substringAfter(SagaParticipantsRegistry.SAGA_REQUESTORCHESTRATOR_SAGA, "direct:");

from(SagaParticipantsRegistry.SAGA_REQUESTORCHESTRATOR_SAGA)
.routeId(routeId)
.saga()
.completionMode(SagaCompletionMode.AUTO)
.propagation(SagaPropagation.REQUIRED)
.end()
.process(e -> beforeSaga(e))
.multicast()
.parallelProcessing()

.to(SagaParticipantsRegistry.SAGA_CREATECUSTOMER_EXECUTE)
.to(SagaParticipantsRegistry.SAGA_EXECUTEPAYMENT_EXECUTE)
.end()
.process(e -> afterSaga(e))
.end();
}

In this example the participants will be called in parallel. Additionally, a beforeSaga() and afterSaga() method will be implemented in the Orchestrator Configurator service. The beforeSaga() method will call the respective command to store a long running action ID as the Saga’s identifier in the Saga entity.

protected void beforeSaga(Exchange exchange) {
SagaDomainServicesInput sagaInput = exchange.getMessage().getBody(SagaDomainServicesInput.class);
String lraUrl = exchange.getIn().getHeader(Exchange.SAGA_LONG_RUNNING_ACTION, String.class);

SetLongRunningActionIdInput commandInput = new SetLongRunningActionIdInputEntity();
commandInput.setLraId(extractLRAId(lraUrl));
customerRegistrationSagaCommand.setLongRunningActionId(sagaInput.getCustomerRegistrationSaga(), commandInput);
}

private String extractLRAId(String lraUrl) {
if (lraUrl != null && lraUrl.isBlank()) {
return null;
}

String[] split = StringUtils.split(lraUrl, "/");
if (ArrayUtils.isEmpty(split)) {
return null;
}
return split[split.length - 1];
}


The afterSaga() method will be called by the LRA Coordinator after the execution for all participants has finished successfully. In this method, the Saga entity’s state will be set to completed. Since both the onComplete and onCompensate methods will always be called by the LRA Coordinator in parallel along with all participant’s onComplete and onCompensate methods, there is no implementation necessary for this example. In case you need to perform some logic that can be done simultaneously to the participant’s completion or compensation methods, you can implement these methods in your orchestrator service and add either .completion(SAGA_...COMPLETE) or .compensation(SAGA..._COMPENSATE) or both after the .propagation(SagaPropagation.REQUIRED) to the route in your configure() method. In this diagram, you can see the workflow for a successful Saga transaction:

Services Overview

This diagram illustrates the workflow for an unsuccessful Saga transaction, along with the compensation steps:

Services Overview

In this scenario, the Customer Participant throws an error during the execute() method. Therefore, the participant’s status will be set to FAILED in the method’s catch-block. The LRA Coordinator registers the exception in the Customer Participant and waits for both execute() methods to terminate, after which the onCompensate() for every participant is immediately called. Here, the compensation logic is implemented. First, the participant’s status is checked. If it is not FAILED, then its execute method has successfully been executed, meaning that compensation calls need to be made. In that case, for the Payment Participant a compensation payment is executed, whereas for the Customer Participant the deletion of the created customer is triggered.

At the end of each onCompensate method, the participant’s status is updated and a Kafka event is published. The Registration Service’s agent consumes the event in its onMessage method and iterates all participants to check if each one is in an appropriate state. In this example, the state should be either COMPENSATED or FAILED. If that is the case, the agent calls the respective command to set the Saga to FAILED and finalize the transaction.

In case there is an error during any onCompensate method, the LRA Coordinator will repetitively call the method until it completes. The SAGA can be initiated by calling the execute method of the generated class called RequestOrchestrator. This will trigger the execute method of all participants defined in the SAGA.

🌟result

Congratulations! You have learned how to design transaction-like behaviour using the Saga pattern support in your Java domain service project.