Cadence is a reliable orchestration engine for executing asynchronous business logic. It is well suited for implementing orchestration-based sagas because it simplifies the coding of compensation logic, has built-in support for unlimited exponential retry attempts, and guarantees that the workflow code eventually completes. In addition, it gives full visibility into the state of each workflow, unlike queue-based orchestration, where getting the current status of each individual request is almost impossible.
We need a Cadence server running locally and the easiest way is to run it in Docker. Download the docker-compose file from the official repository and start the services:
Next, we need to register a domain for further use.
We could do this from Java, but since this is a one-time operation, we will do it using the CLI.
Let’s register a domain with the name
We will implement all services in one maven project to simplify the example and make it easier to share dependencies as well as some classes.
We need to include Cadence Java Client and some dependencies that it relies on:
<dependency> <groupId>com.uber.cadence</groupId> <artifactId>cadence-client</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>1.10</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
We will also add Spring Boot web starter to create a rest controller and Lombok for convenience:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.4.5</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>provided</scope> </dependency>
Finally, we exclude the debug logs by adding the following logback config file to the classpath:
We will implement the sample e-commerce application provided on the Saga pattern description page:
Order Servicereceives the
POST /ordersrequest and starts the
Create Ordersaga orchestrator
- The saga orchestrator creates an
- It then sends a
Reserve Creditcommand to the
Customer Serviceattempts to reserve credit
- It then sends back a reply message indicating the outcome
- The saga orchestrator either approves or rejects the
Before moving on to implementing services, let’s take a look at a few Cadence concepts, which include workflows and activities. A workflow is a fault-oblivious stateful abstraction that is not allowed to call an external API directly due to deterministic execution requirements. Instead, it organizes the execution of activities, which can be a function or method in one of the supported languages and can contain any code without restriction.
From this, we can conclude that we need to define and implement activities in each of our services participating in the saga, and then define and implement a workflow that will orchestrate these activities.
Activities are defined as methods of an interface, and each method defines a single activity type. Since these interfaces are also required on the client side to invoke the activity, we will put them in a common package.
Customer Service should attempt to reserve credit and return the result:
Order Service should create new pending orders and approve or reject them based on the response from the
Workflows are also defined in the interface, but unlike activities, in addition to the entry point method, they can contain additional methods to react to external signals and query requests, so each method must be annotated accordingly.
We are not interested in additional features and will only use the
In addition to the activity interfaces, our services will share some constants, such as the domain name that we created on the Cadence Server, as well as the names of task lists, which are queues used to deliver tasks to workers:
3.4 Customer Service
First, we will add a very simple implementation of the activity interface so as not to overload the example with extraneous logic:
After that, we need to register the implementation with a worker that connects to Cadence. Using the default options, it will connect to the locally running server:
3.5 Order Service
Next, we will also implement the activities of the
We could implement the workflow in a separate service, but we will add it to the
Order Service and make it also responsible for orchestrating the saga.
Workflow.newActivityStub, which takes activity type and activity options as arguments to create client-side stubs for our activities.
In the implemented method, we instantiate the saga object with default parameters, call the order activity to create a new order and register its rejection as compensation,
then we try to reserve credit and, if successful, approve the order and return its ID, otherwise we throw an exception that,
like any unexpected exception, gets caught by the
try-catch block which triggers a rollback of the saga, performing all registered compensations:
We are planning to use Spring Boot in this service, so its main class will be slightly different from the previous one.
We again create a worker and register the activities, but we also need to register the workflow.
In addition, we do it using the
CommandLineRunner, not in the main method, and we separate the creation of the
WorkflowClient into a bean, since we will need it later:
Finally, we create a controller in which we implement two ways to start workflow execution, synchronously and asynchronously.
We will use a client-side stub to start a workflow that we instantiate by using
but note that on each request we create a new stub to start a new workflow, and not reconnect to the same one:
We can now start both of our services and make sure everything works.
First, let’s make a request to start the workflow asynchronously:
If we take a look at the logs of our services we will see something like:
19:58:28.081 [Activity Executor taskList="OrderTaskList", domain="example": 1] INFO d.s.c.s.s.order.OrderActivitiesImpl - Order 85a203c5-3147-4078-8a00-eb9eeada5494 created in pending state 19:58:28.288 [Activity Executor taskList="CustomerTaskList", domain="example": 1] INFO d.s.c.s.s.c.CustomerActivitiesImpl - Credit reserved for customer 0123 19:58:28.579 [Activity Executor taskList="OrderTaskList", domain="example": 1] INFO d.s.c.s.s.order.OrderActivitiesImpl - Order 85a203c5-3147-4078-8a00-eb9eeada5494 approved
Now let’s try to run the workflow synchronously:
We have successfully received an order ID in response and the logs correspond to the previous ones:
19:58:46.432 [Activity Executor taskList="OrderTaskList", domain="example": 2] INFO d.s.c.s.s.order.OrderActivitiesImpl - Order c5fdd661-6618-44ea-9723-966a5e5f7390 created in pending state 19:58:46.502 [Activity Executor taskList="CustomerTaskList", domain="example": 2] INFO d.s.c.s.s.c.CustomerActivitiesImpl - Credit reserved for customer 3456 19:58:46.559 [Activity Executor taskList="OrderTaskList", domain="example": 2] INFO d.s.c.s.s.order.OrderActivitiesImpl - Order c5fdd661-6618-44ea-9723-966a5e5f7390 approved
Finally, let’s try to create an order with an excess of the credit limit:
In the case of a request for synchronous execution, an internal server error is expected, because we are not handling exceptions properly, and in the logs we should see something like:
19:59:00.621 [Activity Executor taskList="OrderTaskList", domain="example": 3] INFO d.s.c.s.s.order.OrderActivitiesImpl - Order 884c1def-cd52-4efa-8853-35fd19ff60e8 created in pending state 19:59:00.762 [Activity Executor taskList="CustomerTaskList", domain="example": 3] INFO d.s.c.s.s.c.CustomerActivitiesImpl - Credit limit is exceeded for customer 5678 19:59:00.876 [Activity Executor taskList="OrderTaskList", domain="example": 3] INFO d.s.c.s.s.order.OrderActivitiesImpl - Order 884c1def-cd52-4efa-8853-35fd19ff60e8 rejected
We can also check the status and details of the executed workflows in the Cadence Web UI. To do this, in the browser, go to http://localhost:8088/domains/example/workflows
We’ve implemented an orchestration-based saga using Cadence, but we’ve covered a small fraction of the capabilities of this promising platform that can be adapted for a wide variety of use cases and really deserves a look.
Full source code can be found on GitHub.