As software engineers, we write software to generate business value for a longer period of time, which means the software should be maintainable. To achieve maintainable and sustainable software, there are some principles and goals that should be applied in disciplined software engineering. A main challenge of disciplined software engineering is keeping changes locally (see e.g. Single Responsibility Principle) and keeping your business rules and domain code stable as well as maintainable.
If we follow these rules, the changes to migrate or update your project from Camunda Platform 7 to Camunda Platform 8 should not affect all your code, especially not your domain centered code. This goal can be achieved, for example, by using the clean architecture. With this blog post and a code example, I would like to show you how this can be achieved. The migration plan I propose assumes a clean architecture or similar separation of business and framework code, e.g. the hexagonal architecture pattern. Nevertheless, the underlying principle, like e.g. the dependency inversion principle, can be applied to any software project using other architectural approaches like a multitier architecture.
Bernd Rücker also describes this idea of separating “your actual business logic from the delegates and all Camunda APIs” in his blog post “What to do When You Can’t Quickly Migrate to Camunda 8”. I have taken the topic of “clean delegates” a bit further and will describe with clean architecture a possibility to implement the goals that Bernd Rücker also mentions.
Camunda Platform 7 and Camunda Platform 8
In April 2022, Camunda released their new Camunda Platform 8 (a new “Universal Process Orchestrator” era). Camunda Platform 8 has been developed from scratch based on the experiences from Camunda Platform 7. The biggest change is Zeebe as remote engine.
Today the recommended Camunda Platform 7 Greenfield stack is Camunda Run with external Task workers (see deciding about your Camunda 7 stack). Before the release of Camunda Platform 8, the recommended Greenfield stack was an embedded engine, which means you add your process engine as dependency to your own application resulting in a single deployment. The use of an embedded engine was a very commonly used approach and is usually accompanied by the use of JavaDelegates. Most Camunda Platform 7 projects probably use the embedded engine approach. The example shown in this blog post is therefore based on this approach, but can easily be applied to the external task worker pattern as well.
Camunda Platform 8, using Zeebe as remote engine, does not offer the possibility to use JavaDelegates to execute code, e.g. during ServiceTasks. Zeebe uses job workers to perform (e.g., complete) tasks in a process. The concept of job workers is comparable to the external task worker in Camunda Platform 7. To learn more about the changes from Camunda Platform 7 to 8 I suggest starting with the documentation migrating from Camunda Platform 7.
In this blog post, I only focus on the developer API changes which come along with the migration to Camunda Platform 8, like e.g. using the
ZeebeClient instead of the
RuntimeService to start a message correlation. Beside that, there are some more changes like GraphQL for the Tasklist API, gRPC as protocol for the Zeebe job worker API and the usage of Kubernetes.
Motivation for Clean Architecture
A software architecture that integrates changeability in their package and class structure makes it easy to switch or migrate a framework. So the migration of Camunda Platform 7 to 8 is much less painful with a good architecture.
Robert C. Martin describes architectural guidelines in his book “Clean Architecture”, which should allow independence of frameworks, databases, user interface (UI) and other technologies. In his opinion, clean architecture ensures the testability of business rules by its design. The image above displays layers as concentric circles wrapping each other. Each layer represents different parts of software. The center of the circle represents “policies” and thus your business rules and domain knowledge. The outer circles are “mechanisms” supporting our domain center. Beside the layers, the arrows show the dependency rule – only inward point dependencies! To reach the goals of clean architecture, the domain code must not have any dependencies pointing outwards. Instead, all dependencies point towards the domain code.
The essential aspect of your business domain is placed in the core of the architecture: the entities. They are only accessed by the surrounding layer: the use cases. Services in a classic layered architecture represent use cases in a clean architecture, but these services should be more fine-grained so that they have only one responsibility. You do not want one big service implementing all your business use cases. Supporting components are placed around the core (your entities and use cases), such as persistence or user interfaces.
The Dependency Inversion Principle
When the dependency rule is applied, the domain has no knowledge about how you persist your data or how you display them in any client. The domain should not contain any framework code (arguably Dependency Injection). As I already mentioned, you could use the Dependency Inversion Principle (DIP) to apply the dependency rule of clean architecture. The DIP tells you to reverse the direction of a dependency. You may be thinking of the Inversion of Control (IoC) design pattern, which is not the same as DIP, although they fit well together. If you want to know the exact differences I recommend reading Martin Fowlers article “DIP in the Wild” (in short: “[…] IoC is about direction, and DIP is about shape.”). The following figure shows an example of how the DIP works.
Imagine having a service (
DomainService in the image) which starts a Camunda Process. To isolate your service (your business logic) from the framework, you could create another service using the Camunda Java API to start a process instance. The left frame of the image shows this scenario without applying the DIP. The domain service calls the
ProcessEngineService directly. So what’s the problem? Starting a process is a core aspect of our domain, so we want to pull it into our domain. By doing so, we break the rule of keeping our domain framework-agnostic. We can fix this, by placing an interface in our domain core instead of the concrete implementation and place the actual implementation outside of our domain layer, et voilà we apply the DIP.
Combining the DIP with the Ports and Adapters architecture (the clean architecture emerged of), we get the picture shown below.
Separating our ports / use cases and adapters that drive our application (input-ports) or are driven by our application (output-ports) helps us to structure our code even more and to keep the boundaries more clear.
Mapping between layers
The following image shows how the layers interact with the domain object with and without mapping. Without mapping, you miss the biggest advantage of clean architecture: decoupling your domain core with the outer (infrastructure) layers. If you do not map between your inner and outer layers, you are not isolated. If a third-party system changes its data model, your domain model needs to change as well. To prevent dependence on external influencing factors and to promote independence and decoupling, it is necessary to map between the layers. Using the input and output ports (use case layer) as gatekeepers into your domain core, they define how to communicate and interact with your application. They provide a clear API and by mapping into your domain you keep it independent of any framework or technology model changes.
The frame explaining the mapping approach is just one possible way of mapping. If you want to know more about different stages of mapping, take a look a Tom Hombergs book “Get Your Hands Dirty on Clean Architecture”, he explains them pretty well.
In conclusion, mapping can be used to achieve greater decoupling. On the other hand, mapping between each layer could produce a lot of boilerplate code, which might be overkill depending on your use case and the goals you are working towards.
Replacing the adapter when upgrading to Camunda Platform 8
The Diff-View below shows a symbolic example: Migrating a JavaDelegate to a job worker. It shows how the JavaDelegate implementation disappears. Instead, a method annotated with @ZeebeWorker handles all actions which should happen during a service task, for example. The snippet also shows that if the code is structured correctly, migration does not require any deep changes to the domain core: The action of the service task (picking a content) does not change, because the clean service task calls the underlying port to our domain. By changing the adapter implementation – the interaction with your process engine – the implementation of the domain does not change. We still call the same Interface
So, applying clean architecture means the only changed files are all placed in the adapter layer (and the BPMN model, of course). So the domain does not need to be touched to change a framework. All necessary change to upgrade from Camunda Platform 7 to 8 in the underlaying example project are shown in this pull request. The only changed files are all placed in the adapter layer. So the domain does not need to be touched to change a framework.
This was just a quick summary of how to migrate to Camunda Platform 8. Take a closer look at my example project if you want to deep dive the changes. Clean architecture is assumed in the proposed migration plan. Nevertheless, as described before, the principles can also be applied to other architectural approaches, such as a multi-level architecture.
The domain core of a software development project must be kept isolated to keep it stable, so you can switch e.g. more easily between frameworks and keep your domain clean. By applying the DIP in an architectural style like clean architecture, we can decouple our domain logic from any framework or e.g. persistence and UI-specific problems, which reduces the changes required and isolates them to the outer layers.
Take a look at the example project that demonstrates how clean architecture helps to upgrade from Camunda Platform 7 to 8, especially at the pull request, which demonstrates that only the outer layers need to be touched and your business code remains stable.
And as always: We are happy to support you with our BPM Technology Consulting.