Spring Boot and Temporal
I’m a Spring Boot fan
And I am not alone.
Spring is objectively the most popular Java framework out there, and in this year’s SpringOne keynote, Purnima Padmanabhan, the General Manager and VP of the VMware Tanzu division – the place where the biggest part of the Spring team lives – reported that 46% of enterprise workloads use Spring.
That is 46% of the workloads. That means that nearly half the time when you are using your banking, insurance, medical records, telco and utilities apps, Spring is in play. Streaming? Yeah, Netflix is all in on Spring. ECommerce? Amazon heavily uses Spring.
And there is good reason for this popularity.
Not only does Spring Boot provide abstractions that implement a whole lot of super valuable capabilities, like database and other persistence access, RESTful services, batch processing, and so much more, it also does many other things really well, like dependency management and packaging.
Spring is my go to.
So when I set out to apply Temporal to the example project that runs through my book, I honestly didn’t even think about it – I assumed Spring Boot.
Quick review of the service structure
To remind you, in that experiment (see blog) I used Temporal to bring some resilience to a request/response style web service, moving the business logic that had been in the web service controller into a workflow. I now had two java applications – one for the HTTP API and another for the workflow. The former became a workflow client, kicking off the workflow when the web service was invoked.
While I’m doing experiments on my laptop today, we have to think about how these apps will eventually be deployed in a production setting, and for java that is a fat jar. I won’t go into a long explanation of the value of a fat jar, except to say that particularly in the containerized world we live in, having a single, self-contained artifact that brings ALL of it’s dependencies is clearly the way to go.
Okay, so in the above diagram, my goal is to package each of the java applications as jars. In effect I want to have a controller.jar
and a workflow.jar
. My project already built the controller as a jar – using the Spring Boot Maven plugin (see pom file). The question that remained was how I would create the fat jar for the worker/workflow.
Spring Boot and Temporal don’t get along – or do they?
As I am still a newbie to Temporal, I built my first Temporal workers using tutorials like this one (project repo). This project includes pom files that are optimized, not for jar packaging, but rather for running the worker locally. Most of the tutorials make use of the maven exec plugin which, for java, does the magic of building your classpath and then supplying that to java
run locally (BTW, I had never used this maven plugin before and once I understood what it was doing, I thought it was pretty darn cool). Sure, you can create a jar by running mvn clean package
but the resulting jar is not a fat jar – it contains only the class files for the worker, workflow and activity classes. You can run that jar file, but then you have to draw together all of the dependencies and supply them (note previous mention of the maven exec plugin doing that magic for you). As I said, fat jars are the way to go.
I might have mentioned 😉, my go-to for all things java is Spring, so I set out to see what the Temporal/Spring Boot story was, and I got mixed messages. I found forum posts that advised folks not to use Spring with Temporal, but these posts were a bit old and comments from people like Maxim and Tihomir promised future native support for Spring in Temporal. I also found some mention of people using the two together, though I was unable to find any comprehensive explanation of how to do this.
So I worked on it a bit and here’s the skinny.
Spring Boot and Temporal DO work together just fine, if you know what you are doing.
Remember, that Spring Boot does many things. In the remainder of this post I’ll explain how I used it to solve a number of different problems.
My initial goal was the fat jar
I love Spring, but I’m not a zealot – in fact, I’m not a zealot for any tech. Even the coolest technology is only of real interest if it solves a problem. The first problem that I wanted to solve was the creation of that fat jar. Given the mixed messages I was getting around Spring, I thought it was a good opportunity to see what else was out there.
Shade and Assembly Maven Plugins – tl;dr; they didn’t work for me
In some of the aforementioned forum posts, folks reported using both of these maven plugins, and with little trouble I was able to build a fat jar using the maven shade plugin. But then running the jar didn’t work – it crashed on the worker factory start. I did a bit of googling and came across this forum post where I saw both some of the aforementioned reports of Spring success and someone cautioning use of the shade plugin. So I didn’t dig much deeper.
I then gave the maven assembly plugin a try and got exactly the same results – the jar was built but when I ran it, it crashed on the worker factory start,
Spring Boot Maven Plugin – tl;dr; it just worked
Inspired by the comments in the above linked forum post, I decided to give Spring Boot a try. I’ve built many a jar file using the Spring Boot Maven plugin so I just dropped the following into my pom.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
That’s right, got rid of all of the other plugins that I had lifted from the Temporal sample apps, and replaced them with this. Ran this:
mvn clean package spring-boot:repackage
And then this:
java -jar target/connectionposts-workflow-2.0-SNAPSHOT.jar
And voilà, my worker was running! Have I mentioned that I love Spring?
Quick note: In the above maven command you see the spring-boot:repackage
part – this tells maven to have Spring create the fat jar. Without this modifier, it will still create a jar file with only this project’s classes. If you had the following also added to the pom, then the Spring Boot repackaging would automatically run. The parent establishes some default goals.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/>
</parent>
Problem solved – I had a working fat jar, built using Spring.
But you know what?…
I wasn’t really using Spring with Temporal yet
Well, that is not entirely true. I was absolutely using parts of Spring. In my worker, more specifically in my Activities, I was already using the Spring framework to make HTTP requests, using things like the RestTemplate and the ResponseEntity, but my app wasn’t a Spring Boot app. And you know what? It was working fine. Did I really need to turn this into a Spring Boot app?
Well, I had another problem to solve.
Like every good developer, I initially hard coded a few things – specifically URLs for downstream services. I’ll remind you that I built all of this on chapter 5 code which hadn’t yet addressed application configuration or service discovery – so the code brought those values in through application.properties
files. But I had previously injected those values into my controller using Spring Boot.
So that was the next problem to solve: how to draw values from a property file into workflow and/or activity code.
Sure, Spring Boot is not the only way to do such things, but it does it well and, given my underlying desire to really understand Spring and Temporal together, I thought this a great opportunity to dig deeper. And this meant turning my worker app into a spring boot app.
Turning my Workflow into a Spring Boot App
Spring Boot does so many things. It provides great programming abstractions – like the ones I was already using for making HTTP requests. It has great packaging tools – like the Spring Boot Maven plugin that I used to package my fat jar.
But perhaps one of the most useful things Spring Boot offers is the lifecycle management of objects – or as Spring calls them, beans. While not all class instances/objects in a Spring app need to be beans, Spring beans benefit from a host of other things like dependency injection, auto wiring, and application configuration through property files.
But allowing a framework to lifecycle manage objects is exactly what the previously mentioned forum posts warned against, because Temporal manages the lifecycle of workflows itself, and it really wants to control what is injected into those instances.
After lots of digging, reading and experimenting, here’s the conclusion I’ve reached: You can have Spring Boot manage the lifecycle of Workers and Activities, but NOT workflows.
In the final implementation, I ended up creating a Spring Boot application for my worker, but did not create Spring Beans for my workflow nor for my activities. Using a standard Spring Boot application structure, I created a PostAggregatorWorkerConfig
class where I defined beans for the worker (and a few other things); this config basically creates the things that had previously been created directly within the worker app. Then the PostAggregatorWorker
has the main method that simply starts the Spring container with the beans defined in the config class. As a part of this, the worker is created and is listening to the appropriate task queue on the Temporal server.
With the application configuration being a Spring bean, we can inject values including those application properties, and I’ve used Spring @Value
annotation to do so. As you can see here, I placed those annotations on parameters that are passed to the Spring config class method that ultimately creates the worker – Spring Boot auto-wires the arguments of that method for us.
@Bean
public Worker worker(WorkerFactory factory,
@Value("${connectionpostscontroller.connectionsUrl}") String connectionsUrl,
@Value("${connectionpostscontroller.postsUrl}") String postsUrl,
@Value("${connectionpostscontroller.usersUrl}") String usersUrl,
@Value("${INSTANCE_IP}") String ip,
@Value("${INSTANCE_PORT}") String p) {
...
Within that method we create an instance of the PostAggregatorActivitiesImpl
and pass in those parameters:
worker.registerActivitiesImplementations(
new PostAggregatorActivitiesImpl(connectionsUrl, postsUrl, usersUrl, ip, p));
The activities defined in the PostAggregatorActivitiesImpl
now have the URLs they need to make calls to the down-stream services.
The following diagram depicts the configuration that I’ve just described.
Hard-coded URLs gone! 🙌
Summary
Java and Spring Boot remain widely used technologies – in fact, I’d venture to say that they are dominant in large corporate entities, and I have some anecdotal evidence this is true in the public sector as well, where software is increasingly being custom built (examples). As Temporal gains traction, how it is used in conjunction with Spring Boot needs to be well understood and where necessary, fine tuned. That is precisely what I aimed for in my explorations and this blog post – to understand. The good news is that it works well.
In the end, my project leverages Spring Boot is several ways:
- It uses abstractions to simplify the code that makes REST requests, specifically using classes like RestTemplate and ResponseEntity.
- It uses the Spring Boot Maven plugin to create the fat jar that encapsulates all dependencies for my worker/workflow app.
- And making the worker a Spring Boot App allowed me to leverage familiar mechanisms for injecting configuration, property values in particular, into my app.
Rumors of Spring Boot and Temporal incompatibilities are unfounded. In fact, let me come back to that aforementioned promise of tighter integration, as there has been progress. There are two Spring Boot related modules in the Temporal java-sdk project (starter, autoconfigure). They had the “alpha” designation taken off of them in June of this year, though they are still in public preview. I’ve only taken a quick glance, as I wanted to understand the relationship first hand, without additional abstractions doing magic for me. But as this gains traction I’m sure we’ll get some of that fine tuning I mentioned. I’ll definitely be taking a closer look and providing feedback!
I want to close with two things.
Something I learned
This is the first time that I have used Spring to create a fat jar for a non-Spring application, and it worked great! The Spring Boot Maven plugin does a really nice job with dependency management and packaging, including a java class loader that makes the fat jar executable.
Something I can use help with
Y’all read my comments in here about lifecycle management of various parts of a Temporal workflow. By all reports, and my personal experience, Temporal really only lifecycle manages the workflows, allowing things like workers and activities to be lifecycle managed by something else. That could be you, in your code, or it could be through the use of something like Spring Boot, like I’ve done here.
What I would love help with is to understand the details of the lifecycle management that Temporal does for workflows. Can you point me to some docs? Sequence or other types of diagrams? Even videos? That would be amazing. TIA.
Finally, some references
Most of these are linked throughout the post, but for easy access:
- My sample application
- The complete project with all three microservices
- In particular, here is the post aggregation service in which I have applied Temporal
- Within that, here is the workflow implementation.
- (Note, my previous blog talks a bit about how I structured my repo for this webservice.)
- Through my research I found a few other samples that gave me hints on how to proceed
- A minimalistic sample app that Tihomir put together that shows Spring Boot with Temporal. Slightly different structure than what I have used, but it helped.
- A much more substantial project that demonstrates many things beyond the basics.
- The Temporal Spring Boot integration pieces; these are both in the java-sdk repo:
Share Your Thoughts