Workflow Orchestration with Temporal and Spring Boot

Pankaj Kumar
9 min readOct 29, 2022

In the context of any software application, a workflow (also known as a business process) is a repeatable sequence of steps to fulfil a certain business use case. Typically, a workflow can be long-running (from seconds to days) and involve calls to unreliable external systems (such as the payment system), introducing a failure scenario. In the case of a microservice application, which is deployed as a collection of small, independent, and loosely coupled services, this failure can be tough to handle as workflow steps can span many services.

In this article, we’ll understand how to implement resilient and fault-tolerance workflow in a microservices architecture using Temporal, a durable, reliable, scalable workflow orchestrator engine. We will understand the core concept of Temporal and see code examples of Spring Boot microservices and Temporal.

What is Temporal?

Temporal is an open-source, workflow-as-code, scalable, and distributed workflow orchestration platform. One of the main benefits of the workflow as code approach is that all dev tools like IDEs, debuggers, compilers, and unit testing frameworks work out of the box.

In Temporal, you can define workflow steps in code using constructs of language-specific client SDK. The client SDKs are available in many languages, including Go, Java, Python, PHP, and TypeScript. You can check the full list of supported SDKs at Temporal SDK.

Once the client application submits the workflow execution to Temporal Server, it runs workflow in a distributed manner. It handles all failure scenarios, such as retries, state management, deadlines, backpressure, etc. Temporal Workers execute the workflow and Workflow steps (called Activities).

The Temporal server maintains the state of workflow execution in event sourced database. Because of the replay feature of event sourcing, the Temporal server can continue execution from the point it left in case of an outage in the Temporal Server or Workers.

One important difference between Temporal and other workflow engines, such as Airflow and Netflix Conductor, is that it doesn’t need special executors; workers are part of your application code. Thus, it fits nicely with microservices applications.

If you have used Cadence earlier, you will find many similarities between Temporal and Cadence. In fact, Temporal is a fork of Cadence with some notable differences.

Temporal Architecture

On a high level, Temporal consists of two distinct components — Temporal Server and Temporal Client components (running as part of your application’s deployment). The client applications communicate to Temporal Server using the client SDK via gRPC.

The internals of the Temporal Server and communication protocols are completely hidden from developers. As a result, application developers just need to make themselves aware of the APIs of the client SDKs.

The Temporal server primarily consists of a web console to visualize and troubleshoot workflow execution, a bunch of services written in go, and an event-sourced database.

On the client application side, the main Temporal components are — Workflow, Activities, and Workers.

Temporal Workflow

In Temporal, we define workflow in code (also called Workflow Definition). Temporal Workflow is an instance of workflow definition (executed by a Worker).

When a client application submits the Workflow to the Temporal Server, it becomes the responsibility of the Temporal Server to execute the Workflow and all Activities associated with the Workflow in the appropriate Worker.

Example of a Workflow defined in Java:

In the above example, the workflow definition consists of four distinct steps (Activities).

Temporal Activities

An Activity is a function that executes a single, well-defined action (either short or long-running), such as calling another service, transcoding a media file, or transforming data. You can think of an Activity as a distinct step (business action) in a workflow execution.

In the above workflow definition, there are four different Activities — Debit Payment, Reserve Inventory, Ship Goods, and Local Activity completeOrder.

A Local Activity is an Activity that executes in the same process as the Workflow that spawns it. You should use Local Activities for very short-living tasks that do not need flow control, rate limiting, and routing capabilities.

Temporal Worker

A Temporal Worker is responsible for executing Workflows and Activities. A Worker connects to the Temporal server via client SDK and polls the server for the workflow or activities tasks. You can register Workflows and Activities with the Worker using APIs provided by Client SDK.

Use Case and Code Example

The working code example of this article is listed on GitHub. To run the example, clone the repository and import order-fulfillment-workflow as a project in your favourite IDE as a Gradle project.

All microservices and integration with Temporal are implemented in Spring Boot and Temporal Java client SDK in this code example.

Code structure

The sample use case covered in this article is order fulfillment workflow in the microservices application (as shown below).

In this use case, once a user checks out goods from the cart, UI calls the Order microservice to create an order. The Order microservice first creates an order in Pending status and then submits the Order fulfillment workflow to Temporal (steps 3 to 6 in the above diagram).

As this article shows the concept of distributed workflow orchestration, we’ll focus on workflow orchestration and not on the actual business logic. Also, we will not cover any failure and compensation scenarios.

Order Fulfillment Workflow Definition

In Temporal, workflows constituents an interface, annotated with @WorkflowInterface and exactly one workflow method annotated with @WorkflowMethod, and its implementation.

Once we have defined the workflow interface, we can implement workflow with appropriate business action (Activities) as:

As mentioned earlier, an Activity represents a single, well-defined action. Think of an Activity as a step of the business process (as represented by the workflow).

Before using Activities in the Workflow, we need to create an Activity stub by calling Workflow.newActivityStub(Class,..). For example, you can implement OrderFulfillmentWorkflowImpl with DebitcPaymentcActivity as:

In the above code,

  • Start-To-Close Timeout is the maximum time allowed for a single Activity task execution. We should always set this value to be longer than the maximum possible time for the Activity execution.
  • A Task Queue is a lightweight, dynamically allocated queue that Worker(s) poll for Tasks (Workflow and Activities). In the above code example, Payment Activity is sent to the PAYMENT_ACTIVITY_TASK_QUEUE, and the Payment Service Worker can subscribe to the task queue as workerFactory.newWorker(TaskQueue.PAYMENT_ACTIVITY_TASK_QUEUE.name(), workerOptions).
  • The RetryOptions determines how an Activity is retried in the case of failure.

In the above use case, DebitPaymentActivity is called by the Order microservice but its implementation is at the Payment microservice codebase. How can the Order microservice refer to the classes from the Payment microservice? One way to solve this problem is by using an untyped stub Workflow.newUntypedActivityStub or we can package the Activity definition as a common library that can be referred to by required microservices.

Workflow Constraints

One of the most important aspects of implementing Workflow in Temporal is ensuring it exhibits certain deterministic traits — i.e., ensuring that the rerunning same Workflow produces the same result. For example, using random values, local time, etc., causes non-deterministic behaviour.

In general, avoid using database calls, random values, time-clocks, external service calls, shared mutable states, etc., in the Workflow implementation.

Spring and Temporal: as Temporal controls the Workflow life cycle, we need to ensure that a Workflow implementation is not created as Singleton Bean and that there is no shared mutable state defined in the Workflow.

Workflow Activities

The Activities in Temporal is defined as an interface annotated with @ActivityInterface. For example, you can define DebitPaymentActivity as:

Workflow Activity Implementation

In general, Activities perform domain-specific tasks and thus are associated with domain-specific microservices. For example, all payment-related tasks are the responsibility of Payment microservices. So, it’s natural for us to consider the implementation of DebitPaymentActivity as part of the Payment microservice.

In Spring Boot, you can define Activities as any normal singleton bean along with dependency injection of other beans such as Services and Repositories.

For example, we can implement DebitPaymentActivity as:

Where DebitPaymentActivity bean is defined as:

Worker Implementation

In Temporal, Workers are responsible for executing Workflow or Activities tasks. Workers poll Task Queue for any tasks and execute tasks once available.

You can register all Activities executed by a Worker as worker.registerActivitiesImplementations(Object..).

Similarly, you can register Workflow with the Worker as

worker.registerWorkflowImplementationTypes(Class<?>...);

A Worker can execute Activities or Workflow or both. Also, you can register more than one Activity or Workflow with a Worker.

As Workers are part of your application code, there is nothing special needed to create a Worker except getting WorkflowClient and registering Activities or Workflow with the Worker.

Below is the code of PaymentWorker that executes DebitPaymentActivity.

And code for workflowOrchestratorClient.getWorkflowClient().

Where the target is the URL of the Temporal server. If you are running Temporal locally using docker-compose, then you can define the target as target=127.0.0.1:7233.

Unlike Workflow, a Worker can be created as a normal Spring Bean and there is no restriction about using dependency injection.

Submitting Workflow

To submit workflow execution to the Temporal server, we first need to create a stub of the Workflow by calling workflowClient.newWorkflowStub(Class<T>,..) and call WorkflowClient.start(..) for asynchronous execution of the workflow.

Where WorkflowOrchestrator is called from the domain command object OrderCommandImpl.

It’s always a good idea to abstract infrastructure-specific code from core business logic by using design practices such as Hexagonal Architecture. This gives us the flexibility to change the Workflow Orchestrator, for example, Temporal with Airflow, without affecting business logic.

The workflow submission sequence diagram looks like this.

You can start the workflow execution by calling the REST API of the Order Service (using Curl or Postman) as:

curl --location --request POST 'localhost:8081/orders' \ --header 'Content-Type: application/json' \ --data-raw '{ "productId": 30979484, "price": 28.99, "quantity": 2 }'

Temporal with Spring Boot

At the time of writing this article, official support for native Temporal Spring Boot integration is in progress. But, integrating Spring Boot with Temporal is not that difficult, provided we care for certain things.

Firstly, we need to make sure that the Workflow implementation, when registered with Workers as worker.registerWorkflowImplementationTypes (Class<?>..), doesn't contain any mutable state or Spring dependency (such as any Repository, Service, or any other object managed by spring).

Secondly, make sure to use prototype bean when Workflow is registered with the Worker using the Factory method.

worker.addWorkflowImplementationFactory( WorkflowImplementationOptions options, Class<R> workflowInterface, Func<R> factory) {

There is no restriction in Activities and Worker implementation, i.e. Activities and Workers can be created as default singleton Spring Bean.

Installing Temporal

The fastest (and perhaps easiest) way to install Temporal locally is by using Docker as,

git clone <https://github.com/temporalio/docker-compose.git> 
cd docker-compose
docker-compose up

Once Temporal Server is started, you can open Temporal UI by visiting http://localhost:8080/namespaces/default/workflows

Temporal User Interface

Temporal UI provides some important detail about the Workflow and Activities execution.

Visualize all Workflow and its status.

Details of individual Workflow.

Worker and Task Queue.

Details of Activity and its input.

Summary

  • Temporal is an open-source, workflow-as-code workflow orchestration platform.
  • In Temporal, you can define workflow steps (called Activities) in code using constructs of language-specific client SDK.
  • An Activity is a function that executes a single, well-defined action (either short or long-running), such as calling another service, transcoding a media file, or transforming data.
  • A Temporal Worker is responsible for executing Workflows and Activities. A Worker connects to the Temporal server via client SDK and polls the server for the workflow or activities tasks.

Originally published at https://techdozo.dev on October 29, 2022.

--

--

Pankaj Kumar

Software Architect @ Schlumberger ``` Cloud | Microservices | Programming | Kubernetes | Architecture | Machine Learning | Java | Python ```