Quick Example: MySQL

Stage-1 [Minimal Implementation]

This example demonstrates a simple implementation of StackSaga’s synchronous orchestration using MySQL as the database (event-store).

Getting Started - Initial project setup

At this demo we use Spring Mvc as the web framework and MySQL as the primary database for the event-store.

So, To get started, create a new Spring Boot application and include the following dependencies in your pom.xml:

It is recommended to StackSaga Initializer to get the dependency snippets for your project for the StackSaga related dependencies, as it will ensure you have the correct versions and configurations for your project setup.
<dependencyManagement> (1)
    <dependencies>
        <dependency>
            <groupId>org.stacksaga</groupId>
            <artifactId>stacksaga-bom</artifactId>
            <version1.0.0-SNAPSHOT</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency> (2)
        <groupId>org.stacksaga</groupId>
        <artifactId>stacksaga-spring-boot-starter</artifactId>
    </dependency>
    <dependency> (3)
            <groupId>org.stacksaga</groupId>
        <artifactId>stacksaga-mysql-reactive-support</artifactId>
    </dependency>
</dependencies>
1 This section imports the StackSaga BOM (Bill of Materials) which manages the versions of all StackSaga related dependencies, ensuring compatibility and simplifying dependency management.
2 This is the main StackSaga Spring Boot Starter dependency that includes the core functionalities of StackSaga, such as the orchestration engine, event handling, and transaction management. It provides the necessary components to implement the StackSaga pattern in your application.
3 This dependency provides support for using MySQL as the event-store in a reactive manner. It includes the necessary components and configurations to integrate MySQL with StackSaga, allowing you to store and manage events in a MySQL database while leveraging reactive programming paradigms. Stacksaga MySQL Reactive Support

Creating the Domain-Entity

Domain-Entity is the main entity that will be used in the transaction flow, it represents the main data structure that will be manipulated and processed throughout the transaction. read more

@Getter
@Setter
@SagaDomainEntity(
        version = @SagaDomainEntityVersion(major = 1, minor = 0, patch = 0),
        name = "OrderDomainEntity"
)(1)
public class OrderDomainEntity extends DomainEntity { (2)

    // Domain-specific fields
    @JsonProperty("username")
    private String username;
    @JsonProperty("user_validated")
    private boolean userValidated;
    @JsonProperty("total_amount")
    private double totalAmount;
    @JsonProperty("payment_reference_id")
    private String paymentReferenceId;
    @JsonProperty("product_items")
    private List<String> productItems;

    (3)
    public OrderDomainEntity() {
        super(OrderDomainEntity.class);
    }
}
1 The @SagaDomainEntity annotation marks this class as a domain entity for StackSaga, which means it will be used to represent the state and data of the transaction as it flows through the different steps of the saga. The version attribute allows you to manage changes to the domain entity over time.
2 The OrderDomainEntity class extends the DomainEntity base class provided by StackSaga, which includes common functionality and properties needed for domain entities.
3 The constructor calls the superclass constructor with the class type, which is necessary for StackSaga to properly manage and serialize the domain entity during the transaction flow.

Creating the Executors

The next step is to create the executors for each span (atomic steps) of the LRT (long-running transaction). Executors are responsible for executing the business logic of each step in the transaction flow. read more

In this quick example, we will create 3 executors, one for validating the user, revering order and another for processing the make payment. (it is only for demonstration purposes, in a real-world application you would have more complex logic and more steps in the transaction flow).

Creating ValidateUserExecutor

ValidateUserExecutor is responsible for validating the user information in the transaction flow. It checks if the user is valid and updates the domain entity accordingly.

(1)
@SagaExecutor(executeFor = "user-service", value = "ValidateUserExecutor")
public class ValidateUserExecutor implements QueryExecutor<OrderDomainEntity> { (2)
    @NonNull
    @Override
    (3)
    public ProcessStepManager<OrderDomainEntity> doProcess(
            OrderDomainEntity currentDomainEntityState,
            ProcessStepManagerUtil<OrderDomainEntity> stepManager,
            String idempotencyKey
    ) throws RetryableExecutorException, NonRetryableExecutorException {
        //call the user-service via http or rpc to validate the user information.
        try {
            //simulate some delay for make the request
            Thread.sleep(new Random().nextInt(1000, 3000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        (4)
        return stepManager.next(ReserveOrderExecutor.class, () -> "USER_VALIDATED");
    }
}
1 The @SagaExecutor annotation marks this class as an executor for StackSaga, specifying the service it executes for (user-service) and the name of the executor (ValidateUserExecutor).
2 The ValidateUserExecutor class implements the QueryExecutor interface, which is used for executors that perform read-only operations or validations without modifying the domain entity state. read more
3 The doProcess method is the main entry point for executing the primary logic of the executor. It takes the current state of the domain entity, a step manager utility for managing the flow, and an idempotency key to ensure that the operation can be retried safely if needed. this is the where you would implement the logic to validate the user information, such as calling an external service or performing checks against a database.
4 The stepManager.next method is called to indicate that the next step in the transaction flow should be executed, which in this case is the ReserveOrderExecutor. The lambda function provides event-name indicating that the user has been validated.
The 1st executor can be a QueryExecutor or a CommandExecutor. there is no any restriction on the type of the 1st executor in the transaction flow.

Creating ReserveOrderExecutor

ReserveOrderExecutor is responsible for reserving the order in the transaction flow. It checks if the order can be reserved and do the necessary operations to reserve the order, updating the domain entity accordingly.

@SagaExecutor(executeFor = "order-server", value = "ReserveOrderExecutor") (1)
public class ReserveOrderExecutor implements CommandExecutor<OrderDomainEntity> { (2)
    @NonNull
    @Override
    (3)
    public ProcessStepManager<OrderDomainEntity> doProcess(
            OrderDomainEntity currentDomainEntityState,
            ProcessStepManagerUtil<OrderDomainEntity> stepManager,
            String idempotencyKey
    ) throws RetryableExecutorException, NonRetryableExecutorException {
        //call the internal service and reserve the order
        try {
            //simulate some delay for make the request
            Thread.sleep(new Random().nextInt(1000, 3000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        (4)
        return stepManager.next(MakePaymentExecutor.class, () -> "ORDER_RESERVED");
    }

    @NonNull
    @Override
    (5)
    public SagaExecutionEventName doRevert(
            NonRetryableExecutorException primaryExecutionException,
            OrderDomainEntity finalDomainEntityState,
            RevertHintStore revertHintStore,
            String idempotencyKey
    ) throws RetryableExecutorException {
        //call the internal service to revert the reserve order action
        try {
            //simulate some delay for make the request
            Thread.sleep(new Random().nextInt(1000, 3000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        (6)
        return () -> "ORDER_REVERTED";
    }
}
1 The @SagaExecutor annotation marks this class as an executor for StackSaga, specifying the service it executes for (order-server) and the name of the executor (ReserveOrderExecutor).
2 The ReserveOrderExecutor class implements the CommandExecutor interface, which is used for executors that perform state-changing operations on the domain entity. read more
3 The doProcess method is the main entry point for executing the primary logic of the executor. It takes the current state of the domain entity, a step manager utility for managing the flow, and an idempotency key to ensure that the operation can be retried safely if needed. this is where you would implement the logic to reserve the order, such as calling an external service or performing operations against a database to reserve the order. at this case the order service is the own service of the orchestrator application, so you can call the order service directly without any network call.
4 The stepManager.next method is called to indicate that the next step in the transaction flow should be executed, which in this case is the MakePaymentExecutor.
5 The doRevert method is called when the transaction flow needs to be rolled back due to a failure in a subsequent step. It takes the exception that caused the rollback, the final state of the domain entity, a revert hint store for storing any necessary information for the revert operation, and an idempotency key. this is where you would implement the logic to revert the reserve order action, such as calling service or performing operations against a database to undo the reservation. at this case the order service is the own service of the orchestrator application, so you can call the order service directly without any network call.
6 The lambda function provides the event-name indicating that the order has been reverted.

Creating MakePaymentExecutor

MakePaymentExecutor is responsible for processing the payment in the transaction flow. It checks if the payment can be processed and do the necessary operations to process the payment, updating the domain entity accordingly.

@SagaExecutor(executeFor = "payment-server", value = "MakePaymentExecutor") (1)
public class MakePaymentExecutor implements CommandExecutor<OrderDomainEntity> { (2)
    @NonNull
    @Override
    (3)
    public ProcessStepManager<OrderDomainEntity> doProcess(
            OrderDomainEntity currentDomainEntityState,
            ProcessStepManagerUtil<OrderDomainEntity> stepManager,
            String idempotencyKey
    ) throws RetryableExecutorException, NonRetryableExecutorException {
        //call the payment-service via http or rpc or with any protocol to make the payment.
        try {
            //simulate some delay for make the request
            Thread.sleep(new Random().nextInt(1000, 3000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        (4)
        //simulate payment failure for 50% of the time to test the retry mechanism
        if (new Random().nextBoolean()) {
            throw NonRetryableExecutorException
                    .buildWith(new RuntimeException("insufficient balance"), () -> "MAKE_PAYMENT_FAILED")
                    .build();
        } else {
            return stepManager.complete(() -> "MADE_PAYMENT");
        }
    }

    @NonNull
    @Override
    (5)
    public SagaExecutionEventName doRevert(
            NonRetryableExecutorException primaryExecutionException,
            OrderDomainEntity finalDomainEntityState,
            RevertHintStore revertHintStore,
            String idempotencyKey
    ) throws RetryableExecutorException {
        throw new UnsupportedOperationException("this will not be executed until there is another executor after this executor in the flow.");
    }
}
1 The @SagaExecutor annotation marks this class as an executor for StackSaga, specifying the service it executes for (payment-server) and the name of the executor (MakePaymentExecutor).
2 The MakePaymentExecutor class implements the CommandExecutor interface, which is used for executors that perform state-changing operations on the domain entity. read more
3 The doProcess method is the main entry point for executing the primary logic of the executor. It takes the current state of the domain entity, a step manager utility for managing the flow, and an idempotency key to ensure that the operation can be retried safely if needed. this is where you would implement the logic to process the payment, such as calling an external payment service. in this example, we simulate a payment failure for randomly to test the retry mechanism of the StackSaga engine. if the payment processing fails, we throw a NonRetryableExecutorException with an event name indicating that the payment failed. if the payment processing succeeds, we call stepManager.complete to indicate that the transaction flow is complete, with an event name indicating that the payment was made. if the payment processing fails, the StackSaga engine will automatically trigger compensation by calling the doRevert method of the previous executors in reverse order, starting with the ReserveOrderExecutor to revert the order reservation.
RetryableExecutorException is not simulated in stage. RetryableExecutorException is used in next stage with retry-subsystem.
4 The lambda function provides the event-name indicating that the payment was made or failed.
5 The doRevert method is called when the transaction flow needs to be rolled back due to a failure in a subsequent step. It takes the exception that caused the rollback, the final state of the domain entity, a revert hint store for storing any necessary information for the revert operation, and an idempotency key. in this example, since the MakePaymentExecutor is the last step in the transaction flow, there is no need to implement the revert logic for this executor, because if the payment processing fails, we want to trigger the compensation for the previous successful step, which is the order reservation, and we want to revert that action by calling the doRevert method of the ReserveOrderExecutor. so we can simply throw an UnsupportedOperationException in the doRevert method of the MakePaymentExecutor to indicate that this will not be executed until there is another executor after this executor in the flow. if there was another executor after this executor in the flow, then we would need to implement the revert logic for this executor as well, to handle the case where the subsequent step fails and triggers compensation. in real payment processing scenarios, you definitely need to implement the revert logic for the payment processing.

Creating the Handler

Handler is responsible for handling the transaction and receiving the transaction events emitted by the StackSaga engine during the transaction flow. See more

@Slf4j
@Component(1)
public class PlaceOrderHandler implements TransactionEventListener<OrderDomainEntity> {(2)
    @Override(3)
    public void onStateChanged(TransactionState<OrderDomainEntity> transactionState) {
        log.info("onStateChanged : {}", transactionState.getCurrentStatus());
    }
}
1 Annotate the class with @Component to make it a Spring bean, so that it can be automatically detected and registered by the Spring container.
2 The PlaceOrderHandler class implements the TransactionEventListener interface, which allows it to listen to transaction events emitted by the StackSaga engine during the transaction flow. read more
NOTE: TransactionEventListener has blocking and non-blocking versions. see the full details in TransactionEventListener and ReactiveTransactionEventListener.
3 The onStateChanged method is overridden to handle the transaction state changes. it receives the current state of the transaction as a parameter, which includes information such as the current status of the transaction, the execution history, the current domain entity state, and the timestamps. in this example, we simply log the current status of the transaction whenever it changes. you can implement any logic you need in this method based on your use case, such as sending notifications to users about the transaction status changes or updating other systems based on the transaction events.

Creating the Controller to trigger the transaction accessing StackSagaTemplate

Here we will create a simple REST controller with an endpoint to trigger the transaction flow by accessing the StackSagaTemplate which is the main API for interacting with the StackSaga engine to execute transactions. read more

This just a regular Spring REST controller with a POST endpoint to place an order, and inside the endpoint method we will access the StackSagaTemplate to start the transaction flow by providing the initial domain entity state and specifying the first executor to execute in the flow.

@Slf4j
@RestController
@RequestMapping("/api/v1/order")
@RequiredArgsConstructor
public class PlaceOrderController {

    private final SagaTemplate<OrderDomainEntity> stacksagaTemplate; (1)

    @PostMapping
    public String placeOrder(@RequestBody PlaceOrderRequest placeOrderRequest) {
        final String transactionId = this
                .stacksagaTemplate
                (2)
                .init(() -> {
                    OrderDomainEntity orderDomainEntity = new OrderDomainEntity();
                    orderDomainEntity.setUsername(placeOrderRequest.getUsername());
                    orderDomainEntity.setTotalAmount(placeOrderRequest.getTotalAmount());
                    orderDomainEntity.setProductItems(Arrays.asList(placeOrderRequest.getItems()));
                    return orderDomainEntity;
                })
                (3)
                .peek(orderDomainEntity -> {
                    //you can do some local operation for the orderDomainEntity before the saga process.
                    //and also here you can access the unique id for the transaction that provide by the engine.
                    log.info("Transaction Id : {}", orderDomainEntity.getTransactionId());
                })
                (4)
                .startWith(ValidateUserExecutor.class)
                (5)
                .fireAndForget()
                (6)
                .execute();
        (7)
        return "Order placed successfully with transaction id: " + transactionId;
    }

    (8)
    @Data
    public static class PlaceOrderRequest {
        private String username;
        private double totalAmount;
        private String[] items;
    }
}
1 Autowired the SagaTemplat<DE> which is the main API for interacting with the StackSaga engine to execute transactions. it provides a fluent API to define and execute the transaction flow. DE is the type of the domain entity used in the transaction flow, which is OrderDomainEntity in this example.
2 The init method is called to initialize the transaction flow with the initial state of the domain entity. it takes a supplier function that returns the initial domain entity state, which is created based on the request data in this example.
3 The peek method is an optional step that allows you to perform some local operations with the domain entity before the saga process starts. it also provides access to the unique transaction id generated by the engine, which can be useful for logging or tracking purposes.
4 The startWith method is called to specify the first executor to execute in the transaction flow, which is the ValidateUserExecutor in this example.
5 The fireAndForget method is called to indicate that the transaction should be executed in a fire-and-forget manner, meaning that the caller does not need to wait for the transaction to complete and can continue with other operations. this is useful for scenarios where you want to trigger the transaction and do not need to wait for the result immediately. and also it is the recommended way to execute long-running transactions to avoid blocking the caller thread. you can get notified about the transaction status frequently by subscribing to the events emitted by the transaction flow via ReactiveTransactionEventListener or TransactionEventListener based on your application type (reactive or servlet).
6 The execute method is called to execute the transaction flow with the defined configuration. it returns the unique transaction id generated by the engine for this transaction, which can be used for tracking and logging purposes.
7 The endpoint returns a response indicating that the order was placed successfully along with the transaction id.
8 The PlaceOrderRequest is a simple DTO class to represent the request body for placing an order, it contains the necessary fields such as username, totalAmount, and items to create the initial domain entity state for the transaction flow. It is not recommended to use the domain-entity class as the request body directly to avoid coupling the API layer with the domain layer, and to have better control over the API contract.

Tune Configuration

To run the application, you need to configure the fallowing properties in your application.properties or application.yml file.

#spring properties
spring.application.name=order-service
#stacksaga-instance properties (1)
stacksaga.instance.cluster=local-cluster
stacksaga.instance.region=local-region
stacksaga.instance.zone=local-zone
#stacksaga-starter properties
stacksaga.domain-entity-scan=org.example.orderservice.domain (2)
#stacksaga-mysql-database-support properties (3)
stacksaga.mysql.r2dbc.url=r2dbc:mysql://localhost:3306
stacksaga.mysql.r2dbc.database=order_service_event_store
stacksaga.mysql.r2dbc.username=${MYSQL_USER:root}
stacksaga.mysql.r2dbc.password=${MYSQL_PASSWORD:password}
(4)
stacksaga.mysql.jdbc.url=jdbc:mysql://localhost:3306/order_service_event_store
stacksaga.mysql.jdbc.username=${MYSQL_USER:root}
stacksaga.mysql.jdbc.password=${MYSQL_PASSWORD:mafei}
1 The stacksaga.instance properties are used to configure the instance information for the StackSaga engine, such as the cluster, region, and zone. these properties are used is used to identify the transactions for retry and re-storing purposes mainly in distributed environments. in this example, we are using a local cluster, region, and zone for demonstration purposes.
2 The stacksaga.domain-entity-scan property is used to specify the package where the domain entity classes are located. the StackSaga engine will scan this package to find the domain entity classes and register them for use in the transaction flow. it can be provided as a comma-separated list of packages if you have multiple packages containing domain entity classes.
3 The stacksaga.mysql.r2dbc properties are used to configure the R2DBC connection for the StackSaga engine to connect to the MySQL database as the event-store. you need to provide the URL, database name, username, and password for the MySQL connection. the event-store should be a separate database schema dedicated for storing the transaction events and data, it should not be the same database schema used by your application for its regular data storage to avoid any potential conflicts and to have better separation of concerns.
4 The stacksaga.mysql.jdbc properties are used to configure the JDBC connection. JDBC connection is used by internally Liquibase to database migration and schema creation.

Running the application and testing the transaction flow

Run the application and test the transaction flow by sending a POST request to the /api/v1/order endpoint with the necessary request body to place an order. try many times until the payment processing fails to see the compensation mechanism in action😉.

  • Without Primary Execution Failure: you can see the following logs

2026-06-21T23:57:36.911+05:30  INFO 19868 --- [order-service] [nio-8080-exec-3] o.e.o.controller.PlaceOrderController    : Transaction Id : e0b34f8a-9702-4e23-87e8-8ae5b9654a61
2026-06-21T23:57:38.357+05:30  INFO 19868 --- [order-service] [oundedElastic-3] o.e.o.handler.PlaceOrderHandler          : onStateChanged : PROCESSING
2026-06-21T23:57:41.177+05:30  INFO 19868 --- [order-service] [oundedElastic-3] o.e.o.handler.PlaceOrderHandler          : onStateChanged : PROCESSING
2026-06-21T23:57:42.527+05:30  INFO 19868 --- [order-service] [oundedElastic-3] o.e.o.handler.PlaceOrderHandler          : onStateChanged : PROCESS_COMPLETED
  • With Primary Execution Failure: you can see the following logs.

2026-06-21T23:54:19.094+05:30  INFO 17792 --- [order-service] [nio-8080-exec-1] o.e.o.controller.PlaceOrderController    : Transaction Id : 0864a84c-79a1-45a1-a688-662f431ca218
2026-06-21T23:54:26.248+05:30  INFO 17792 --- [order-service] [oundedElastic-3] o.e.o.handler.PlaceOrderHandler          : onStateChanged : PROCESSING
2026-06-21T23:54:27.956+05:30  INFO 17792 --- [order-service] [oundedElastic-3] o.e.o.handler.PlaceOrderHandler          : onStateChanged : PROCESSING
2026-06-21T23:54:29.980+05:30  INFO 17792 --- [order-service] [oundedElastic-3] o.e.o.handler.PlaceOrderHandler          : onStateChanged : REVERTING
2026-06-21T23:54:32.546+05:30  INFO 17792 --- [order-service] [oundedElastic-3] o.e.o.handler.PlaceOrderHandler          : onStateChanged : REVERT_COMPLETED

Stage-2 [Adding Retry Capability]

In the previous stage, we have implemented a simple transaction flow for the happy path with a simulation. but in real-world scenarios, you will definitely have some transient failures that can be retried, such as network issues, temporary unavailability of external services, or database deadlocks. so in the next stage we will enhance our implementation by adding retry capability to handle such transient failures and make our transaction flow more resilient. we will implement a retry mechanism that allows us to automatically retry failed steps in the transaction flow based on configurable retry policies. stay tuned😉.

StackSaga has subsystem for handling the retrying. the fallowing are the main steps to add retry capability to the transaction flow:

  1. Create the Ring Coordinator Application by using the stacksaga-ring-coordinator-spring-boot-starter as a separate application that will run alongside your orchestrator application, and it will be responsible for coordinating the token ranges withing the available orchestrator instances in the cluster.

  2. Adding the stacksaga-ring-coordinator-connector dependency to your orchestrator application to enable the integration with the Ring Coordinator, which is responsible for receiving the relevant token range that has been allocated for the instance by the Ring Coordinator(Slave).

Creating the Ring Coordinator Application (Master & Slave Hybrid)

Just create a new Spring Boot application and add the following dependency in your pom.xml to enable the Ring Coordinator functionality. It is not necessary to have the web or any other dependencies. but you are totally free to add any other dependencies that you need for your application.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.stacksaga</groupId>
            <artifactId>stacksaga-bom</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependency>
    <groupId>org.stacksaga</groupId>
    <artifactId>stacksaga-ring-coordinator-spring-boot-starter</artifactId>
</dependency>
It is recommended to StackSaga Initializer to get the dependency snippets for your project for the StackSaga related dependencies, as it will ensure you have the correct versions and configurations for your project setup.

After adding the dependency, you need to configure the fallowing properties in your application.properties or application.yml file to configure the Ring Coordinator server.

spring.application.name=order-service-ring-coordinator
#rsocket-server for master (1)
spring.rsocket.server.address=localhost
spring.rsocket.server.port=4455
#master and slave both (2)
stacksaga.coordinator.target-services=order-service
stacksaga.coordinator.instance-type=master,slave
#stacksaga-instance properties (3)
stacksaga.instance.cluster=local-cluster
stacksaga.instance.region=local-region
stacksaga.instance.zone=local-zone
#Master connection details for slave to connect to master (4)
stacksaga.coordinator.slave.target-master.port=4455
stacksaga.coordinator.slave.target-master.host=localhost
1 The spring.rsocket.server properties are used to configure the RSocket server for the Ring Coordinator application. you need to provide the address and port for the RSocket server, which will be used by the slave instances to connect to the master instance.
2 The stacksaga.coordinator.target-services property is used to specify the target services that the Ring Coordinator will manage. in this example, we are specifying the order-service as the target service. the stacksaga.coordinator.instance-type property is used to specify the instance type for the Ring Coordinator application, which can be either master, slave both due to sample small size cluster. in a real-world scenario, you would typically have one master instance and multiple slave instances for better scalability and fault tolerance.
3 The stacksaga.instance properties are used to configure the instance information for the Ring Coordinator application. the cluster and the region should be the same as the ones configured in the orchestrator application to ensure that they are in the same cluster and region for proper coordination.
4 Due to the instance acts as s slave as well, it needs to connect to the master instance to receive the token range information, so you need to provide the connection details of the master instance using the stacksaga.coordinator.slave.target-master properties.

Adding stacksaga-ring-coordinator-connector in existing orchestrator

Go to the pom.xml of your existing orchestrator application and add the following dependency to enable the integration with the Ring Coordinator.

<dependency>
    <groupId>org.stacksaga</groupId>
    <artifactId>stacksaga-ring-coordinator-connector</artifactId>
</dependency>

And the update the fallowing properties in your application.properties or application.yml file to configure the Ring Coordinator connection.

#retry properties
stacksaga.mysql.transaction.retry.delay=1m (1)
stacksaga.coordinator.connector.enabled=true (2)
stacksaga.coordinator.connector.master.requester.host=localhost (3)
stacksaga.coordinator.connector.master.requester.port=4455
1 The stacksaga.mysql.transaction.retry.delay property is used to configure how long time the transaction should be frozen without exposing for retrying. as per the configured value, the transaction will be exposed for retrying after one minute.
retry delay property comes with the database support module. the retry mechanism can have some different configurations based on the database support you are using, so it is recommended to check the documentation of the database support module you are using for more details about the available retry configurations.
2 The stacksaga.coordinator.connector.enabled property is used to enable the Ring Coordinator Connector in the orchestrator application. you need to set it to true to turn the standard Orchestrator service into a Retry Node that can receive the token range information from the Ring Coordinator and handle the retrying of transactions accordingly.
3 Host and the port properties for connecting to the master instance. after connection is established, master will suggest an available slave node (at this time only one we have,) and internally it connect with the slave node and subscribe to the token range information.

Simulating retry scenario

Here we are going to update the ReserveOrderExecutor to simulate a transient failure that can be retried, such as a temporary unavailability of an external service. we will throw a RetryableExecutorException at the first attempt and the second attempt (by the retry) will be successful to demonstrate the retry mechanism in action.

@SagaExecutor(executeFor = "order-server", value = "ReserveOrderExecutor")
public class ReserveOrderExecutor implements CommandExecutor<OrderDomainEntity> {


    (1)
    private final AtomicReference<Map<String, Integer>> counter = new AtomicReference<>(new HashMap<>());

    @NonNull
    @Override
    public ProcessStepManager<OrderDomainEntity> doProcess(
            OrderDomainEntity currentDomainEntityState,
            ProcessStepManagerUtil<OrderDomainEntity> stepManager,
            String idempotencyKey
    ) throws RetryableExecutorException, NonRetryableExecutorException {
        (2)
        if (counter.get().containsKey(currentDomainEntityState.getTransactionId())) {
            counter.get().put(currentDomainEntityState.getTransactionId(), counter.get().get(currentDomainEntityState.getTransactionId()) + 1);
        } else {
            counter.get().put(currentDomainEntityState.getTransactionId(), 1);
            throw RetryableExecutorException.of("Simulate a retryable exception for transactionId: " + currentDomainEntityState.getTransactionId());
        }

        //call the internal service and reserve the order
        try {
            //simulate some delay for make the request
            Thread.sleep(new Random().nextInt(1000, 3000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return stepManager.next(MakePaymentExecutor.class, () -> "ORDER_RESERVED");
    }

    @NonNull
    @Override
    public SagaExecutionEventName doRevert(
            NonRetryableExecutorException primaryExecutionException,
            OrderDomainEntity finalDomainEntityState,
            RevertHintStore revertHintStore,
            String idempotencyKey
    ) throws RetryableExecutorException {
        //call the internal service to revert the reserve order action
        try {
            //simulate some delay for make the request
            Thread.sleep(new Random().nextInt(1000, 3000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return () -> "ORDER_REVERTED";
    }
}
1 We are using an AtomicReference to hold a counter map that keeps track of the number of attempts for each transaction id. this is just for simulation purposes to determine when to throw the RetryableExecutorException. in a real-world scenario, you would typically have some actual logic to determine whether to throw a RetryableExecutorException based on the type of failure you encounter, such as catching specific exceptions from external service calls or checking certain conditions.
2 In the doProcess method, we check the counter for the current transaction id. if it is the first attempt (counter value is 1), we throw a RetryableExecutorException to simulate a transient failure that can be retried. if it is the second attempt (counter value is 2), we proceed with the normal flow and call stepManager.next to move to the next step in the transaction flow. this way, we can demonstrate the retry mechanism in action, where the first attempt fails and triggers a retry, and the second attempt succeeds.

Stage-3 [Observability via trace window]

In the previous stages, we have implemented the transaction flow with compensation and retry capabilities. now let’s see how we can observe the transaction flow and its events using the trace window provided by StackSaga. StackSaga Trace-Window is a web portal provided by StackSaga that allows you to visualize and monitor the transaction flows in secure and user-friendly way. it provides a graphical representation of the transaction flow, showing the different steps, their statuses, and the events emitted during the transaction execution. you can use the trace window to gain insights into the transaction behavior, identify any issues or bottlenecks, and monitor the overall health of your transactions. in this stage, we will see how to access the trace window and use it to observe our transaction flows in real-time.

Adding Trace Window Connector

To enable the integration with the trace window, you need to add the following dependency in your orchestrator application’s pom.xml file.

<dependency>
    <groupId>org.stacksaga</groupId>
    <artifactId>stacksaga-trace-window-connector-api</artifactId>
</dependency>

And then configure the following properties in your application.properties or application.yml file to enable the trace window access.

#Trace-Window
stacksaga.trace-window.secure-api=false (1)
stacksaga.trace-window.enable-api=true (2)
1 The stacksaga.trace-window.secure-api property is used to configure whether the trace window API should be secured with authentication and authorization. setting it to false means that the trace window API will be accessible without any authentication, which can be useful for development and testing purposes. however, in a production environment, it is recommended to set this property to true and provide the access token. read more
2 The stacksaga.trace-window.enable-api property is used to enable the trace window API in the orchestrator application. by default api is disabled.

Now restart the server go to Trace-Window and connect the local running server by providing the url http://localhost:8080 and enter the transaction id that you received in the response of the /api/v1/order endpoint to visualize the transaction.