Camunda · Migration
Camunda 7 vs Camunda 8: what actually changes for Java developers
Embedded engine vs Zeebe, JavaDelegates vs job workers, JUEL vs FEEL, Cockpit vs Operate: a practical comparison of Camunda 7 and Camunda 8 for Java developers.
The short answer
Camunda 8 is not an upgrade of Camunda 7 — it is a different engine. Camunda 7 runs a process engine inside (or next to) your Java application, backed by a relational database. Camunda 8 runs Zeebe, a distributed workflow engine your application talks to over the network. Your BPMN diagrams mostly carry over; your Java integration code, transaction assumptions, expressions and operational tooling do not.
With Camunda having announced the end of life of the Camunda 7 Community Edition, many teams are now planning or running migrations — and developers who join those projects need to understand both worlds at once.
Architecture: where the engine runs
In Camunda 7, the engine is a Java library. In the common “embedded” setup it runs in the same JVM as your Spring Boot application and stores process state in your relational database (PostgreSQL, Oracle, MySQL and others) using its own tables. You can also run it as a shared, remote engine and call it over REST.
In Camunda 8, the engine is a separate cluster. Zeebe partitions process state across brokers and replicates it, which is what lets it scale horizontally. Process data for tools like Operate and Tasklist is exported to a search store (Elasticsearch or OpenSearch) rather than queried from a relational database. You can run it as SaaS or self-managed, typically on Kubernetes.
Your code: delegates vs job workers
Camunda 7 calls your code. A service task points at a JavaDelegate bean, and the engine invokes it synchronously while executing the process:
@Component("chargeCard")
public class ChargeCardDelegate implements JavaDelegate {
private final PaymentService paymentService; // constructor omitted
@Override
public void execute(DelegateExecution execution) {
String orderId = (String) execution.getVariable("orderId");
paymentService.charge(orderId);
execution.setVariable("charged", true);
}
}Camunda 8 never calls into your JVM. Instead, your application runs job workers that subscribe to a job type, receive jobs from Zeebe, do the work and report back. With the Spring integration it looks like this:
@Component
public class ChargeCardWorker {
private final PaymentService paymentService; // constructor omitted
@JobWorker(type = "charge-card")
public Map<String, Object> chargeCard(@Variable String orderId) {
paymentService.charge(orderId); // must be idempotent — see below
return Map.of("charged", true);
}
}If you used Camunda 7’s external task pattern, you are already close: external task workers and Zeebe job workers share the same pull-based idea. Execution listeners, task listeners and other engine plugins need more rethinking, because there is no engine inside your application to hook into.
Transactions and consistency
This is the mindset shift that causes the most bugs. In embedded Camunda 7, the engine and your business code can share one database transaction: if your delegate throws, both your data changes and the engine’s state roll back together. Async continuations are how you decide where those transaction boundaries are.
In Camunda 8 there is no shared transaction. Your worker updates your database; Zeebe records the job as completed separately. If your service crashes after charging the card but before completing the job, the job times out and is handed to a worker again. That means:
- Workers must be idempotent. Use a business key (order ID, payment ID) to detect work that was already done.
- Think in terms of retries and compensation, not rollback. Model compensation explicitly in BPMN where a later step can fail.
- Logic that relied on reading engine state inside your transaction needs a new design.
Expressions: JUEL vs FEEL
Camunda 7 uses JUEL (Java Unified Expression Language) in gateways, listeners and input/output mappings. Camunda 8 uses FEEL (Friendly Enough Expression Language) everywhere, and expressions start with an equals sign:
Camunda 7 (JUEL): ${amount > 1000 && customer.vip}
Camunda 8 (FEEL): = amount > 1000 and customer.vipSimple comparisons convert mechanically. Expressions that call Java beans or methods — common in Camunda 7 — have no FEEL equivalent; move that logic into a job worker or a DMN decision instead.
Tooling and operations
- Cockpit → Operate for inspecting process instances and resolving incidents.
- Tasklist exists in both, with different APIs for building your own task UI.
- Optimize for process analytics, and Identity / Console for access and cluster management in Camunda 8.
- APIs: Camunda 7 offers a Java API and REST API over the engine; Camunda 8 clients talk to Zeebe over gRPC, and newer releases add a REST API as well.
Side-by-side comparison
| Topic | Camunda 7 | Camunda 8 |
|---|---|---|
| Engine | Java library, embedded or shared | Zeebe cluster, remote |
| State storage | Relational database | Replicated log on brokers, exported to a search store |
| Integration code | JavaDelegate, listeners, external tasks | Job workers, Connectors |
| Transactions | Can share your DB transaction | No shared transaction; at-least-once jobs |
| Expressions | JUEL | FEEL |
| Monitoring | Cockpit | Operate |
Migration checklist
- Inventory every delegate, listener, script and JUEL expression in your models.
- Decide what happens to running Camunda 7 instances: let them finish, or migrate them.
- Convert BPMN and DMN models, then fix the elements that behave differently.
- Rewrite integration code as idempotent job workers; add compensation where you relied on rollback.
- Translate expressions to FEEL; move bean calls into workers or DMN.
- Rebuild monitoring and alerting around Operate and your exporter’s data.
- Test both happy paths and failure paths — retries, timeouts and incidents.
Camunda also provides migration tooling to analyze and convert models — use it for the mechanical parts, and budget your time for the integration code and the transaction redesign.