In this blog, we discuss workflow orchestration using Cadence and Cassandra. We went through the problems that Cadence workflows help to solve in microservices developments. Also, we looked at the way Cadence is designed that allows us to solve these types of problems. Interestingly, we went a step forward by setting up Cadence locally using Docker images, and we created a simple Cadence workflow to show how Cadence works.
Introduction to Cadence for Workflow Orchestration
As your product workflow continues to grow, you have many services interacting with each other and all these processes must be consistent, you want to ensure the durability of your application, availability, and scalability at all times. The services must communicate and publish information to the next department (service) without any single point of failure, and we know that all these interactions must be consistent at all times. This level of complexity was an example of the complex exchange of services in Uber’s workflow that Uber attempted to solve by developing the powerful orchestration tool called Cadence. Presently, Cadence and Cassandra are being used by different companies and have been proving to be effective. You can check here to further see some of the use cases provided by Uber.
In this article, we are going to look at the following:
- Introduction to Cadence.
- Microservices design patterns.
- Saga Design Pattern.
- Cadence Architecture.
- Cadence setup using Docker images.
- Run a HelloWorld Java Cadence workflow.
1. Introduction to Cadence
According to Uber, Cadence is a fault-oblivious, stateful programming model that obscures most of the complexities of building scalable distributed applications. Cadence provides a durable virtual memory that is not linked to a specific process, and, at the same time, preserves the full application state, including the function stacks, the local variables across all sorts of hosts, and software failures.
Cadence is helpful when your application spans beyond a single request-reply pattern, when you need to track and manage complex states across services (departments), need to respond to asynchronous events, or you need to track and communicate to unreliable external dependencies. In software development, different products are developed for different use cases, we always want to make sure that our design pattern fits our service’s use cases. This is where microservices come in. According to microservices.io, microservices is an architectural style that structures an application as a collection of services that are highly maintainable and testable, loosely coupled, independently deployable, organized around business capabilities, and owned by a small team.
When it comes to the microservices design pattern, there are many different design patterns which we are going to look at in a bit, but it is worth noting that we are going to be looking at the orchestration-based Saga pattern. Saga patterns help to coordinate or manage the notion of transactional consistency across multiple services when a product transaction is made of several distributed steps. These distributed steps must be completed as expected. The way it works is that at the completion of each service of the transaction, events are published to signal completion for the next process/service to be triggered.
These days, no business wants to lose customers, but when the business applications do not respond to queries/requests promptly customers are forced to look for alternatives. In the next section, we have listed the microservices design patterns referencing this great article on microservices design patterns.
2. Microservices design patterns
There are different types of microservices design patterns, they suit different use cases and they are best for particular use cases. Some of the common design patterns are decomposition patterns, database patterns, observability patterns, and cross-cutting concern patterns. For this article, we are going to focus on the database patterns.
Database Patterns
This pattern is also known as the data management pattern and is really an important topic because since we want our system to be distributed and scalable, as such we should have a strategy to handle data in several distributed servers. The main principle of the database pattern is that each microservices should manage its own data, so the data integrity and data consistency should be considered very carefully.
The most common pattern is the database-per-service pattern, the API composition pattern, the Command Query Responsibility Segregation (CQRS) pattern, the event sourcing pattern, the saga pattern, and the shared database anti-pattern. With this in mind, we are going to drill down into the Saga patterns.
3. Saga Design Patterns
A saga pattern is a microservices architecture pattern to implement a transaction/process that spans multiple services. It is a sequence of local transactions with each service performing its own transaction, and publishing events or service information.
Type of Saga Patterns
Choreography: In this approach, there is no central orchestrator, each service participating in the Saga performs its transaction and publishes events.
Orchestration (Cadence): In this approach, there is an orchestrator that manages all the transactions and directs the participating services to execute local transactions based on events. Examples of orchestrators are Temporal and Cadence. In the next section, we are going to look into Cadence closely and learn about its components.
4. Cadence Architecture
The main components of Cadence are the Cadence Server, the Cadence database (back-end), the Cadence Command Line Interface (Cadence CLI), and the Cadence Service (front-end, history, and the matching service), and the Cadence Worker. For the purpose of our implementation, we are going to be using the Apache Cassandra database for the Cadence back-end. Interestingly we can use MySQL or PostgreSQL database instead, but for the purpose of this article, we are going to be using the Cassandra database.
We are going to be writing our Cadence workflow in Java, but you can also program the workflows in the languages of your choice, Go and Java are supported officially, and Python and Ruby are both developed by the Cadence community. You can decide to set up Cadence in a disintegrated way, i.e. install the Cadence server separately, install the Cadence CLI separately, install the Cassandra database using the executable file and connect all these services together. But we are going to deploy using the easier route, we are going to deploy all the components using the docker-compose.yaml file.
5. Cadence Setup using Docker Images
- Start by making a directory for your cadence workflows.
mkdir cadence
- Make sure docker is running locally, if you don’t have it already, please get it from the docker webpage, and get the latest version of the Cadence YAML file that contains the necessary images using the command below.
curl -O https://raw.githubusercontent.com/uber/cadence/master/docker/docker-compose.yml && curl -O https://raw.githubusercontent.com/uber/cadence/master/docker/prometheus_config.yml
- Pull and start all the necessary images in the YAML file
docker-compose up -d
- When the containers are running, then pull the latest Cadence CLI image using the command below
docker run --rm ubercadence/cli:master
- For your Cadence workflow, you must always register a domain for your Cadence workflow, run the following command once before running any new samples with a separate domain. Here we have created a domain called hello-domain.
docker run --network=host --rm ubercadence/cli:master --do hello-domain domain register
Note: If you are getting a connection refused problems with registering a domain, you can go into the docker container and execute the following command to register the workflow domain.
docker exec -it cadence_cadence_1 /bin/bash
Register your domain
cadence --address $(hostname -i):7933 --do hello-domain domain register
- Check to see if the domain has been created.
docker run --network=host --rm ubercadence/cli:master --do hello-domain domain describe
The result will look like this.
6. Run a HelloWorld Java Cadence workflow
We can now write our Cadence Java workflow. You can clone the Java program from the Cadence HelloWorld GitHub repo, and start the Java program, but very importantly make sure you have all the Uber Cadence maven dependencies installed, you can find them here. After you have all the required dependencies, then run the Java workflow, by now your Cadence workflow will be listening for input into the sayHello method.
This is what the HelloWorld Cadence workflow looks like.
import com.uber.cadence.client.WorkflowClient;
import com.uber.cadence.client.WorkflowClientOptions;
import com.uber.cadence.serviceclient.ClientOptions;
import com.uber.cadence.serviceclient.WorkflowServiceTChannel;
import com.uber.cadence.worker.WorkerFactory;
import com.uber.cadence.worker.Worker;
import com.uber.cadence.workflow.Workflow;
import com.uber.cadence.workflow.WorkflowMethod;
import org.slf4j.Logger;
public class App {
public static void main(String[] args) {
WorkflowClient workflowClient =
WorkflowClient.newInstance(
new WorkflowServiceTChannel(ClientOptions.defaultInstance()),
WorkflowClientOptions.newBuilder().setDomain("hello-domain").build()
);
WorkerFactory factory = WorkerFactory.newInstance(workflowClient);
Worker worker = factory.newWorker("helloTASK");
worker.registerWorkflowImplementationTypes(HelloWorldImpl.class);
factory.start();
HelloWorld helloworld = new HelloWorldImpl();
helloworld.sayHello("Cadence");
}
private static Logger logger = Workflow.getLogger(App.class);
public interface HelloWorld {
@WorkflowMethod
String sayHello(String name);
}
public static class HelloWorldImpl implements HelloWorld {
@Override
public String sayHello(String name) {
logger.info("Data passed to UI");
return "Welcome to Cadence Workflow, " + name + "!";
}
}
}
- Now let us run our workflow and pass in the sayHello method name parameter from the CLI.
docker run --network=host --rm ubercadence/cli:master --do hello-domain workflow start --tasklist helloTASK --workflow_type HelloWorld::sayHello --execution_timeout 20 --input \"Isaac\"
Now look into the Java workflow log in the CLI, you should see this at the part of the Cadence logs.
The result of the workflow will be shown on the Cadence UI, you can access the UI at http://localhost:8088/
Result of the workflow.
There are lots of different sample workflows on the Uber Github repository page, and there is also a nice explanation about Cadence and Cassandra use cases here as well. But in all, I will advise that you should stick to writing your workflow in Java and Go, as these languages have more which implies more active developments. Also, there are many code samples of how you can write a solid workflow with Java on cadenceworkflow.io.
Cadence and Cassandra are powerful tools that help to solve many problems associated with distributed and scalable microservices, and it in fact worth investing time into these great tools. One last thing to note is that Cadence is actively been developed by the community, so new features are added regularly, you can as well suggest a feature to the Cadence community. The code for this article is available on Github.
Cassandra.Link
Cassandra.Link is a knowledge base that we created for all things Apache Cassandra. Our goal with Cassandra.Link was to not only fill the gap of Planet Cassandra but to bring the Cassandra community together. Feel free to reach out if you wish to collaborate with us on this project in any capacity.
We are a technology company that specializes in building business platforms. If you have any questions about the tools discussed in this post or about any of our services, feel free to send us an email!