Many Camunda 7 users are currently looking into migration. In the typical migration journey we see an optional preparation step that helps many customers, especially if they still need some time to start their migration efforts. In this step, the existing Camunda 7 solution is refactored to be easier to migrate to Camunda 8 in a next step. This preparation has advantages: You can do those refactorings probably on-the-side while you do changes in your solution anyway. You work in your known environment. And you can still run all automated test cases without touching them, derisking any changes you do.
In this post we want to look at the typical preparation steps (basically those shall get your solution closer to what we call migration-ready solutions):
- Extract complex code from your JavaDelegates into Camunda-independent classes (for example, Spring beans) that are then invoked from the original JavaDelegate. This way, you do a step towards Clean Delegates that will be easy to refactor. The same holds true if you use External Tasks of course.
- Remove the usage of internal APIs or calls that depend on the transactionally integrated architecture of Camunda 7 (for example, querying the HistoryService within a JavaDelegate and expect it to know current changes already).
- Move client API calls to a common place, for example a dedicated CamundaService class. This allows you to replace that CamundaService with a Camunda 8 implementation without touching too many places in the code. Of course this might be limited to common calls (starting a process instance, completing a task, …), otherwise you quickly replicate the full Camunda 7 API, which doesn’t make much sense.
- Adjust your expressions to use FEEL instead of JUEL and especially also remove any calls to Spring beans within your expressions, as this is not possible in Camunda 8. If you need to call Java code you should do this in a listener, and save the result in a process variable you can access in your expression.
- Adjust the data used in your processes. In Camunda 7, you can use everything as process variables, including binary data blobs and serialized Java objects. This is not possible in Camunda 8, and you might need to adjust your process variable strategy. This might be best done in a migration step, for example when moving your runtime data from Camunda 7 to Camunda 8 using the Data Migrator, but probably you can already prepare some of those changes and clean up process variables within Camunda 7 using its API.
- Adjust your BPMN model. One example is that you might want to have additional steps in your model where you can “collect” process instances for migration. Specifically you can then suspend a job in that step, so that runtime instances pile up there, which can help you migrate fewer different scenarios, easing the whole approach.
The rest of the post goes into a bit more detail on some of those points.
Clean Delegates
With Java Delegates and the workflow engine being embedded as a library, projects can do dirty hacks in their code. Casting to implementation classes? No problem. Using a ThreadLocal or trusting a specific transaction manager implementation? Yeah, possible. Calling complex Spring beans hidden behind a simple JUEL (Java unified expression language) expression? Well, you guessed it — doable!
Those hacks are the real show stoppers for migration, as they simply cannot be migrated to Camunda 8. Actually, Camunda 8 increased isolation intentionally.
So you should concentrate on what a Java Delegate is intended to do:
- Read variables from the process and potentially manipulate or transform that data to be used by your business logic.
- Delegate to business logic — this is where Java Delegates got their name from. In a perfect world, you would simply issue a call to your business code in another Spring bean or remote service.
- Transform the results of that business logic into variables you write into the process.
Here’s an example of an ideal Java Delegate:
@Component
public class CreateCustomerInCrmJavaDelegate implements JavaDelegate {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private CrmFacade crmFacade;
public void execute(DelegateExecution execution) throws Exception {
// Data Input Mapping
String customerDataJson = (String) execution.getVariable("customerData");
CustomerData customerData = objectMapper.readValue(customerDataJson, CustomerData.class);
// Delegate to business logic
String customerId = crmFacade.createCustomer(customerData);
// Data Output Mapping
execution.setVariable("customerId", customerId);
}
}
And you should never cast to Camunda implementation classes, use any ThreadLocal object, or influence the transaction manager in any way. Java Delegates should further always be stateless and not store any data in their fields.
The resulting delegate can be easily migrated to a Camunda 8 API.
No transaction managers
You should not trust ACID transaction managers to glue together the workflow engine with your business code. Instead, you need to embrace eventual consistency and make every service task its own transactional step. If you are familiar with Camunda 7 lingo, this means that all BPMN elements will be async=true. A process solution that relies on five service tasks to be executed within one ACID transaction, probably rolling back in case of an error, will make migration challenging.
Don’t expose Camunda API
You should try to apply the information hiding principle and not expose too much of the Camunda API to other parts of your application.
In the following example, you should not hand over an execution context to your CrmFacade, which is hopefully intuitive anyway:
// DO NOT DO THIS!
crmFacade.createCustomer(execution);
The same holds true for when a new order is placed, and your order fulfillment process should be started. Instead of the front-end calling the Camunda API to start a process instance, you are better off providing your own endpoint to translate between the inbound REST call and Camunda, like this for example:
@RestController
public class OrderFulfillmentRestController {
@Autowired
private ProcessEngine camunda;
@RequestMapping(path = "/order", method = PUT)
public String placeOrder(String orderPayload, HttpServletResponse response) throws Exception {
// TODO: Somehow extract data from orderPayload
String orderData = "todo";
ProcessInstance pi = camunda.getRuntimeService() //
.startProcessInstanceByKey("orderFulfillment", //
Variables.putValue("order", orderData));
response.setStatus(HttpServletResponse.SC_ACCEPTED);
return "{\"status\":\"pending\"}";
}
}
Use primitive variable types or JSON
Camunda 7 provides quite flexible ways to add data to your process. For example, you could add Java objects that would be serialized as byte code. Java byte code is brittle and also tied to the Java runtime environment. Another possibility is magically transforming those objects on the fly to XML using Camunda Spin. It turned out this was black magic and led to regular problems, which is why Camunda 8 does not offer this anymore. Instead, you should do any transformation within your code before talking to Camunda. Camunda 8 only takes JSON as a payload, which automatically includes primitive values.
In the above example, you can see that Jackson was used in the delegate for JSON to Java mapping:
@Component
public class CreateCustomerInCrmJavaDelegate implements JavaDelegate {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private CrmFacade crmFacade;
public void execute(DelegateExecution execution) throws Exception {
// Data Input Mapping
String customerDataJson = (String) execution.getVariable("customerData");
CustomerData customerData = objectMapper.readValue(customerDataJson, CustomerData.class);
// Delegate to business logic
String customerId = crmFacade.createCustomer(customerData);
// Data Output Mapping
execution.setVariable("customerId", customerId);
}
}
This way, you have full control over what is happening, and such code is also easily migratable. And the overall complexity is even lower, as Jackson is quite known to Java people — a kind of de-facto standard with a lot of best practices and recipes available.
Simple expressions and FEEL
Camunda 8 uses FEEL as its expression language. There are big advantages to this decision. Not only are the expression languages between BPMN and DMN harmonized, but also the language is really powerful for typical expressions. One of my favorite examples is the following onboarding demo we regularly show. A decision table will hand back a list of possible risks, whereas every risk has a severity indicator (yellow, red) and a description.

The result of this decision shall be used in the process to make a routing decision:

To unwrap the DMN result in Camunda 7, you could write some Java code and attach that to a listener when leaving the DMN task (this is already an anti-pattern for migration as you will read next). The code is not super readable:
@Component
public class MapDmnResult implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) throws Exception {
List<String> risks = new ArrayList<String>();
Set<String> riskLevels = new HashSet<String>();
Object oDMNresult = execution.getVariable("riskDMNresult");
for (Object oResult : (List<?>) oDMNresult) {
Map<?, ?> result = (Map<?, ?>) oResult;
risks.add(result.containsKey("risk") ? (String) result.get("risk") : "");
if (result.get("riskLevel") != null) {
riskLevels.add(((String) result.get("riskLevel")).toLowerCase());
}
}
String accumulatedRiskLevel = "green";
if (riskLevels.contains("rot") || riskLevels.contains("red")) {
accumulatedRiskLevel = "red";
} else if (riskLevels.contains("gelb") || riskLevels.contains("yellow")) {
accumulatedRiskLevel = "yellow";
}
execution.setVariable("risks", Variables.objectValue(risks).serializationDataFormat(SerializationDataFormats.JSON).create());
execution.setVariable("riskLevel", accumulatedRiskLevel);
}
}
With FEEL, you can evaluate that data structure directly and have an expression on the “red” path:
= some risk in riskLevels satisfies risk = "red"
Isn’t this a great expression? If you think, yes, and you have such use cases, you can even hook in FEEL as the scripting language in Camunda 7 today (as explained by Scripting with DMN inside BPMN or User Task Assignment based on a DMN Decision Table).
But the more common situation is that you will keep using JUEL in Camunda 7. If you write simple expressions, they can be easily migrated automatically, as you can see in the test case of the migration community extension. You should avoid more complex expressions if possible.
Very often, a good workaround to achieve this is to adjust the output mapping of your Java Delegate to prepare data in a form that allows for easy expressions.
You should definitely avoid hooking in Java code during an expression evaluation. The above listener to process the DMN result was one example of this. But a more diabolic example could be the following expression in Camunda 7:
#{ dmnResultChecker.check( riskDMNresult ) }
Now, the dmnResultChecker is a Spring bean that can contain arbitrary Java logic, possibly even querying some remote service to query whether we currently accept yellow risks or not (sorry, this is not a good example). Such code can not be executed within Camunda 8 FEEL expressions, and the logic needs to be moved elsewhere.
Camunda Forms
Finally, while Camunda 7 supports different types of task forms, Camunda 8 only supports Camunda Forms (and will actually be extended over time). If you rely on other form types, you either need to make Camunda Forms out of them or use a bespoke Tasklist where you still support those forms.
Summary
In today’s blog post, I wanted to show you some preparation steps you might want to take for a Camunda 7 to 8 migration. This also hints at how a solution should look to be easier to migrate.
With Java Delegates, you have to be very mindful to avoid hacks that will hinder a migration to Camunda 8. This article sketched the practices you should stick to in order to make migration easier whenever you want to do it, which is mostly about writing clean delegates, sticking to common architecture best practices, using primitive values or JSON, and writing simple expressions.
As always, I am happy to hear your feedback or discuss any questions you might have.
Editor’s note: This post has been updated as of April 2025 for clarity and accuracy.
Start the discussion at forum.camunda.io