Versioning - Java SDK
Since Workflow Executions in Temporal can run for long periods — sometimes months or even years — it's common to need to make changes to a Workflow Definition, even while a particular Workflow Execution is in progress.
The Temporal Platform requires that Workflow code is deterministic. If you make a change to your Workflow code that would cause non-deterministic behavior on Replay, you'll need to use one of our Versioning methods to gracefully update your running Workflows. With Versioning, you can modify your Workflow Definition so that new executions use the updated code, while existing ones continue running the original version. There are three primary Versioning methods that you can use:
- Workflow Type Versioning. This is the simplest of the three, and acts more like a cutover than true versioning. It is suitable for short-running Workflows.
- Branching with GetVersion. This method works by adding branches to your code tied to specific revisions. In most other SDKs, it is called patching.
- Worker Versioning. The Worker Versioning feature allows you to tag your Workers and programmatically roll them out in deployment versions, so that old Workers can run old code paths and new Workers can run new code paths. If you were using this method experimentally prior to summer 2025, refer to the Worker Versioning Legacy docs.
Workflow Type Versioning
Since incompatible changes only affect open Workflow Executions of the same type, you can avoid this problem by changing the Workflow Type for the new version. To do this, you can copy the Workflow Definition function, giving it a different name, and make sure that both names were registered with your Workers.
For example, if you had made an incompatible change to the following Workflow Definition:
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
@WorkflowInterface
public interface PizzaWorkflow {
@WorkflowMethod
public OrderConfirmation pizzaWorkflow(PizzaOrder order);
}
public class PizzaWorkflowImpl{
@Override
public OrderConfirmation pizzaWorkflow(PizzaOrder order){
// implementation code omitted for this example
}
}
then you would create a separate interface and class as follows:
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
@WorkflowInterface
public interface PizzaWorkflowV2 {
@WorkflowMethod
public OrderConfirmation pizzaWorkflow(PizzaOrder order);
}
public class PizzaWorkflowImplV2 implements PizzaWorkflowV2{
@Override
public OrderConfirmation pizzaWorkflow(PizzaOrder order){
// implementation code omitted for this example
}
}
It is necessary to create a separate interface because a Workflow Interface can only have one Workflow Method.
You can use any name you like for the new function. Using some type of version identifier, such as V2 in this example, will make it easier to identify the change.
You would then update the Worker configuration to register both Workflow Types:
worker.registerWorkflowImplementationTypes(PizzaWorkflowImpl.class);
worker.registerWorkflowImplementationTypes(PizzaWorkflowImplV2.class);
The upside of this versioning method is that it is easy to understand at a glance, as it does not really use any Temporal platform features.
The downside of this method is that it does not use any Temporal platform features.
It requires you to duplicate code and to update any code and commands used to start the Workflow.
This can become impractical over time, depending on how you are providing configuration strings to your deployment.
This method also does not provide a way to introduce versioning to any still-running Workflows -- it is essentially just a cutover, unlike the GetVersion
method.
Branching with GetVersion
Branching with Workflow.getVersion
essentially defines a logical branch for a specific change in the Workflow.
If your Workflow is not pinned to a specific deployment or you need to fix a bug in a running workflow, you can branch it.
Consider the following Workflow Definition:
public void processFile(Arguments args) {
String localName = null;
String processedName = null;
try {
localName = activities.download(args.getSourceBucketName(), args.getSourceFilename());
processedName = activities.processFile(localName);
activities.upload(args.getTargetBucketName(), args.getTargetFilename(), processedName);
} finally {
if (localName != null) { // File was downloaded.
activities.deleteLocalFile(localName);
}
if (processedName != null) { // File was processed.
activities.deleteLocalFile(processedName);
}
}
}
Imagine you want to revise this Workflow by adding another Activity to calculate a file checksum.
If an existing Workflow Execution was started by the original version of the Workflow code, where there was no calculateChecksum()
Activity, and then resumed running on a new Worker where this Activity had been added, the server side Event History would be out of sync.
This would cause the Workflow to fail with a nondeterminism error.
Thus we use workflow.GetVersion()
:
public void processFile(Arguments args) {
String localName = null;
String processedName = null;
try {
localName = activities.download(args.getSourceBucketName(), args.getSourceFilename());
processedName = activities.processFile(localName);
int version = Workflow.getVersion("checksumAdded", Workflow.DEFAULT_VERSION, 1);
if (version == Workflow.DEFAULT_VERSION) {
activities.upload(args.getTargetBucketName(), args.getTargetFilename(), processedName);
} else {
long checksum = activities.calculateChecksum(processedName);
activities.uploadWithChecksum(
args.getTargetBucketName(), args.getTargetFilename(), processedName, checksum);
}
} finally {
if (localName != null) { // File was downloaded.
activities.deleteLocalFile(localName);
}
if (processedName != null) { // File was processed.
activities.deleteLocalFile(processedName);
}
}
}
When workflow.GetVersion()
is run for the new Workflow Execution, it records a marker in the Event History so that all future calls to GetVersion
for this change id — checksumAdded
in the example — on this Workflow Execution will always return the given version number, which is 1
in the example.
After you are sure that all of the Workflow Executions prior to version 1 have completed, you can remove the code for that version.
public void processFile(Arguments args) {
String localName = null;
String processedName = null;
try {
localName = activities.download(args.getSourceBucketName(), args.getSourceFilename());
processedName = activities.processFile(localName);
// getVersion call is left here to ensure that any attempt to replay history
// for a different version fails. It can be removed later when there is no possibility
// of this happening.
Workflow.getVersion("checksumAdded", 1, 1);
long checksum = activities.calculateChecksum(processedName);
activities.uploadWithChecksum(
args.getTargetBucketName(), args.getTargetFilename(), processedName, checksum);
} finally {
if (localName != null) { // File was downloaded.
activities.deleteLocalFile(localName);
}
if (processedName != null) { // File was processed.
activities.deleteLocalFile(processedName);
}
}
}
Adding Support for Versioned Workflow Visibility in the Event History
In other Temporal SDKs, when you invoke getVersion
or the patching API, the SDK records an
UpsertWorkflowSearchAttribute
Event in the history.
This adds support for a custom query parameter in the web UI named TemporalChangeVersion
that allows you to filter Workflows based on their version.
The Java SDK does not automatically add this attribute, so you'll likely want to do it manually.
Within your Workflow Implementation code you'll need to perform the following steps:
Import the SearchAttributes
class
import io.temporal.common.SearchAttributeKey;
Define the SearchAttributesKey
object
This object will be used as the key within the search attributes. This is done as an instance variable.
public static final SearchAttributeKey<List<String>> TEMPORAL_CHANGE_VERSION = SearchAttributeKey.forKeywordList("TemporalChangeVersion");
Set the Search Attribute using upsert
You should set this attribute when you make the call to getVersion
.
int version = Workflow.getVersion("MovedThankYouAfterLoop", Workflow.DEFAULT_VERSION, 1);
if (version != Workflow.DEFAULT_VERSION) {
Workflow.upsertTypedSearchAttributes(Constants.TEMPORAL_CHANGE_VERSION
.valueSet(Arrays.asList(("MovedThankYouAfterLoop-" + version))));
}
You should only set the attribute on new versions.
Setting Attributes for Multiple getVersion
Calls
The code in the previous section works well for code that only has one call to getVersion()
.
However, you may encounter situations where you have to have multiple calls to getVersion()
to handle multiple independent changes to your Workflow.
In this case, you should create a list of all the version changes and then set the attribute value:
List<String> list = new ArrayList<String>();
int versionOne = Workflow.getVersion("versionOne", Workflow.DEFAULT_VERSION, 1);
int versionTwo = Workflow.getVersion("versionTwo", Workflow.DEFAULT_VERSION, 1);
if ( versionOne != Workflow.DEFAULT_VERSION ) {
list.append("versionOne-" + versionOne);
}
if (versionTwo != Workflow.DEFAULT_VERSION) {
list.append("versionTwo-" + versionTwo);
}
Workflow.upsertTypedSearchAttributes(Constants.TEMPORAL_CHANGE_VERSION.valueSet(list));
GetVersion allows you to make changes to currently running Workflows. It is a powerful method for introducing compatible changes without introducing non-determinism errors.
Worker Versioning
Temporal's Worker Versioning feature allows you to tag your Workers and programmatically roll them out in deployment versions, so that old Workers can run old code paths and new Workers can run new code paths. This way, you can pin your deployments to specific revisions, often avoiding the need for branching.
Changing the order of any commands in your Workflow code that interact directly with the Temporal Service -- such as calling an Activity or creating a Sleep timer -- will cause a non-determinism error unless you've implemented a versioning solution. To test whether a new revision will require versioning, you should incorporate Replay Testing.