Business Process Modelling using Camunda advanced features
Camunda is a very powerful workflow and decision automation platform. We use Camunda as a process engine in one of our solutions, Digital Product Center and in other custom software development projects for financial and insurance sectors.
Thanks to these rich experiences we wrote a tech blog post about:
- Camunda & .NET Core integration,
- Error handling methods in web apps,
- Our custom extensions for process administrators, operational users and managers.
This post is the next article where Camunda plays a major role. I will talk about elements that may seem unnecessary at the beginning of the road, but with more and more complex business requirements, and thus building more and more complex processes, they become necessary.
In the following short chapters I will describe:
- what are the types of listener and what can we use them for
- why knowledge of how transactions work is crucial to modeling efficient processes
- how you can use a pair of link events to simplify a BPMN diagram
- how timer events work and when they can surprise us’
Listeners
Listeners allow you to execute external Java code or evaluate an expression when certain events occur during process execution.
Listeners are a mechanism that is very often used during process development.
We can compare them to aspects of aspect programming. Comparing to aspects may open up some horizons for you, as many programmers are closer to this solution than Camunda specific listeners.
Depending on the definition, we can run them at start/end activity/gateway/process instance/events. Below I will present you two examples of the use of a listener, which I hope will inspire you to develop your processes in this area.
Logging
One of the most common use cases of using aspects is additional logging (e.g. logging additional technical information or measuring the execution time of a given method/function). We can use listeners for exactly the same.
Imagine that the technical administrator on the client side formulated the requirement: “I would like to log each activity call for each process instance running in a separate file.”
The simplest solution is to add two listeners that will be responsible for it – one for the start activity and second for the end.
@Slf4j @Component public class LoggingExecutionListener implements ExecutionListener { private Expression entryPoint; public void notify(DelegateExecution execution) throws Exception { String processBaseInfo = buildProcessBaseInfo(execution); log.info(processBaseInfo); } private String buildProcessBaseInfo(DelegateExecution execution) { return execution.getProcessBusinessKey() + " " + execution.getProcessInstanceId() + " " + execution.getCurrentActivityName() + " " + entryPoint.getValue(execution).toString(); } }
In the above example, we also use the ability to inject fields inside the listener. We inject a “entryPoint” field of the String type, which allows us to log in whether activity starts or ends.
Notifications
Second example is a listener sending information about a specific pathway to an external module responsible for extended reporting. In a moment you will see what we use such detailed information for in our solutions.
@Component public class SequenceFlowListener implements ExecutionListener { [...] public void notify(DelegateExecution execution) throws Exception { SequenceFlowInfo info = SequenceFlowInfo.builder() .processId(execution.getProcessInstanceId()) .sequenceFlowId(execution.getBpmnModelElementInstance().getId()) .build(); kafkaClient.send(info); } }
This listener builds information about sequence flow and sends this information to Kafka using an injected Kafka client. Reporting module can consume this message from Kafka and save information in the database. Thanks to this information we are able to draw very accurate flow diagrams, perform extensive business analyzes, and then optimize our processes.
Programmers who created advanced solutions based on Camunda may ask here – why don’t you use the activity history for this, which is provided by Camunda out of the box?
We have been using it for a long time, but unfortunately, based on the history of activity, it is very difficult to draw such graphs for processes that use throw / catch events or parallel gateways.
Transaction Boundaries
Transaction boundaries in the process mark places where the current state of the process is saved in the database (the engine commits the transaction). The way the transaction in Camunda worked was a big surprise to me. Take a look at the process below.
At what moments is the current state of the process saved in the database?
After each activity? Or maybe only at the end event?
So, it depends 😉
This graph gives us too little information about the process for us to be able to give one correct answer. We have to go into details. First, we should learn the basic principle on which the transactions in processes are based.
In Camunda docs we can read:
[…] engine runtime will advance in the process until it reaches wait states on each active path of execution. A wait state is a task which is performed later, which means that the engine persists the current execution to the database and waits to be triggered again. […]
What is “wait state”?
The following BPMN elements are always wait states:
This behavior is not desirable in every modeled process. That is why Camunda introduced an additional mechanism that allows you to control transaction boundaries – Asynchronous Continuations. This mechanism allows it to be explicitly said to Camunda – I would like to commit transactions exactly before / after this activity.
I recommend that you carefully read the chapter “Understand Asynchronous Continuations“, which explains in detail how this mechanism works internally.
After this reading, I have a little challenge for you. Try to draw the transaction boundaries in the example process with the following assumptions.
- We assume that we have not applied any additional mechanisms, and all service tasks are delegate expression service tasks.
- Example Service Task 5 is an external task. Example Service Task 2 has a “Asynchronous Before=true”.
- Think up your scenario and check how Camunda will behave 😉
If you have a problem with it – let me know, I will try to help or write another article only on this topic.
Why do I think all of this is so important?
In one of the last projects, we used the delegate expression service task. Each task was related to calling the service from the client’s internal systems. These services were not idempotent. We used services that are responsible for creating/changing business objects in internal systems.
If we were to rely on standard transaction settings in such a situation – we would lead to huge inconsistencies and errors or we would have to design complex compensation events that would introduce additional unnecessary complexity to the business logic. We had to treat each of these services as a separate unit of work.
Link Events
This is a very simple topic, but I think it is worth mentioning. Imagine you have a long, complicated process with dozens of steps. The logic of the process in one of its final steps assumes that in the event of an error, return to one of the first steps. Of course, we can draw an “normal” sequence flow, but with complex diagrams it has a very negative impact on the readability of the entire diagram.
The alternative is to use link events. It serves as a “GoTo” to another point in the same process model. Hence you can use two matching links as an alternative to a sequence flow as shown in the following example.
That’s it 😉
Timer Events
Timer events are events which are triggered by a defined timer. They can be used as start event, intermediate event or boundary event. Boundary events can be interrupting or not.
This is the definition from official Camunda docs.
In the next docs sections, we can read that:
- job executor must be enabled for the timers to work
- timers can count down time in various ways (time, duration, retry)
- ISO 8601 time format is used for configuration
- timers can be of various types (start event, intermediate catching event, boundary event)
At first glance, it might seem that the operation of timers is very simple, even trivial and obvious. The challenges start when we start building processes that are more complex than the typical “Hello world” shown in presentations. Below I present three examples of behaviors that we have dealt with recently.
Business days timers
Some time ago, one of our clients introduced a new requirement.
“We would like some of the timers used to take into account only working days. We do not want to consider weekends or other non-working days, such as public holidays.”
This is a very sensible requirement that may repeat itself in many organizations and their processes. Unfortunately, Camunda out of the box does not offer such possibilities. You have to do some research to meet this requirement. Stephen Russett wrote some tips on Camunda forum. IMHO it’s interesting, but a bit complicated. We decided to approach it a bit differently.
On User Task where we want to catch timer boundary event, we created Input Parameter
(type=script, script format=groovy, script type=inline script)
where we use our custom WorkdayCalculator, save prolonged duration in process variables and then use this value in timer definition
(timer definition type=duration, timer definition= ${execution.getVariable('_prolongedDuration')}).
def workDayCalc = new pl.altkom.software.camunda.WorkdayCalculator() def prolongedDuration = workDayCalc.calculate('PT10D', execution) execution.setVariable('_prolongedDuration', prolongedDuration)
WorkdayCalculator is a class, where we encapsulate business logic responsible for extending the timer depending on the defined working days. There are many ways to implement this logic, we’re not going to go deep into how we did it. If someone needs inspiration, you can take a look here.
10 seconds timer – it works?
“We would like this user task to automatically complete after 10 seconds.”
At first glance, this requirement seems absurd, but believe me – it all depends on the implementation details of the entire solution and how it uses the process engine.
Unfortunately, such a timer does not work out of the box. Why? To understand this, we need to understand how Camunda handles timer events.
The Camunda timer is driven by a job executor. Setting a BPMN timer means that you’re queuing up a sort of “batch job” request to be picked up by the executor the next time it starts-up. The timers are stored in the database as “jobs” to be performed by the job executor. If you are interested, check the tables in Camunda’s database: ACT_RU_JOB and ACT_RU_JOBDEF.
Back to the main topic, when the executor runs, the timer configuration is picked up, evaluated, and then launched (if it’s due for execution).
What are the consequences? Timers don’t execute exactly when you expect 😉
Timer set to 10 seconds may start in the next window set by job-executor configuration – which isn’t necessarily within 10 seconds measured from the token instance arrival at the timer element. If you want high precision, you would probably have to edit the standard job executor settings a lot, which may adversely affect the performance of other parts of the process.
Timer boundary events on service tasks – it works?
“If a service task takes longer than 5 minutes, I’d like to move to an alternate path.”
It works? It depends 😉
It depends on what kind of service we are dealing with.
Thorben Lindhauer, Camunda Developer, explained this case very nicely in one of the threads on Camunda’s forum.
“Timer won’t fire before it gets committed and that the process engine only commits when the process instance reaches a wait state.
asyncBefore won’t help here since it is before and not after start. That means, when the asyncBefore job is created, the service task is not active yet and therefore the timer job does not exist yet.
Timer events on service tasks will only work if the service task itself is a wait state. That is the case if it is implemented as an external task or if it is implemented via the asynchronous service task pattern.”
Async service task is an interesting idea, but you should read the warning in the README: “Warning: this example demonstrates an advanced usage pattern of the process engine based on internal (non-public) API. Similar behavior can be achieved using a send task and a receive task. The latter option is recommended in most use cases.
I have never used this pattern in practice, so I will skip this topic.
In our solutions, we most often use two types of service tasks: delegate expression and external.
In the delegate expression service task we use Java Delegate. Java Delegate is a Java class that implements the Camunda JavaDelegate interface. When we use this type of tasks, Camunda directly calls this method and runs Java code. Camunda processing such a service task is not in the wait state, so as Thorben mentioned, the timers will not work.
When we use external tasks, Camunda does not actively call external service, but it creates a task in its internal database and external service query for new tasks to perform. Once an external service gets a new task, it locks it by the appropriate calling Camunda API, so no other service would take the same task. Finally, when external service performs the task, it informs Camunda by calling another API method. You can read more about this mechanism in Camunda docs. The internal support for external tasks is much more complicated. As Camunda does not actively start external service, it goes into a wait state and the timers on this type of service tasks work properly.
Summary
Camunda is a really powerful tool. It has great configuration possibilities and is easily expandable. The Community version has some shortcomings mainly in the area of reporting and process auditing (that’s why we had to create many plugins to meet the requirements of our clients), but it does not change the fact that it fulfills its tasks perfectly.
In this article, I tried to show you some implementation details of Camunda, which I hope will help you build better, more efficient, observable, less buggy processes. If you liked the article, find it valuable and you would like to read the next part – let me know.
As a programmer who works with Camunda on a daily basis, I encourage everyone to carefully read the documentation (and return to it) and familiarize themselves with the Best Practices from Camunda Consulting Team.
Robert Witkowski
Lead Software Engineer & Team Leader