Why Flowable delegate thread safety matters for process applications
This page covers how Flowable Java delegates behave when several process instances run at the same time, and what to change in your design so shared state cannot leak between executions. Review delegate design whenever a service task calls your own Java class, especially when that class holds fields, caches results or talks to a shared resource. The risk grows as soon as more than one job can run the same delegate at once, which is the normal case for any production deployment that uses asynchronous continuations or timers.
The Flowable async executor runs timers and asynchronous jobs from a thread pool and coordinates multiple nodes through lock owners and lock expiration times stored in the database, as described in the Flowable advanced documentation. That means two jobs for different process instances can reach the same delegate code on different threads, and on different nodes. A delegate that stores an order ID or a customer record in an instance field can therefore hand one customer's data to another instance. Shared-state bugs of this kind often pass unit tests that run one process at a time. Illustratively, a payment-status delegate that writes its result to a field and reads it back after a call to an external service could return another instance's status under load.
Before you build or accept a delegate, confirm three things with your team: which delegate classes hold mutable state, which service tasks mark their transitions as asynchronous, and how many nodes will run the executor against the same database. Check the engine version you are deploying, since your delegate lifecycle assumptions should match that release. The sections below explain how delegate instances and field injection behave, and which patterns keep per-execution data out of shared objects.
Field injection, expressions and Spring beans compared
A class delegate is named in the model with flowable:class, and the engine creates the instance itself. Fields set through field injection come from the BPMN definition, so they are fixed for each service task in the model. Because the engine can reuse a single delegate instance for many executions, any mutable field on that instance is effectively shared state. Two process instances running the same task at the same time can read and write the same object, and the results depend on timing.
- Where state lives: configuration values set at deployment sit in the instance fields, while runtime data belongs in process variables.
- How it is shared: a single instance may serve concurrent executions, so a field written during one execution can be read by another.
- Engine services: avoid storing references to the execution, task or engine services in fields that outlive a single call.
- What to test: run the same service task from several executions in parallel and confirm no value leaks between them.
- Injected values: confirm with the Flowable docs how field injection resolves expressions for your engine version before relying on it.
Before release, check that every field on a class delegate is either immutable or deliberately thread-safe.
With flowable:delegateExpression, the model names a bean expression such as ${orderDelegate}, and the engine resolves that name against the Spring application context. The bean is a regular singleton by default, so its lifecycle belongs to Spring, not to the process. This keeps dependencies, configuration and test doubles in one place. The trade-off is that a singleton bean is shared by every execution unless its scope is changed, which makes stateless design the default requirement.
- Where state lives: collaborators and configuration live in Spring-managed fields that are set once at startup.
- How it is shared: all executions use the same bean, so per-execution data must come from the execution or task passed in each call.
- Scope choices: a prototype or request-scoped bean changes sharing, but confirm the behavior with your Spring version and the Flowable docs.
- What to test: inject test doubles into the bean and run concurrent executions against a real engine to catch shared mutable fields.
- Detail: the sibling topic on Spring beans and expressions covers exposing beans safely and the difference between
expressionanddelegateExpression.
Confirm which bean names your models reference, because a renamed bean fails at runtime rather than at deployment.
An expression attribute on a service task or listener evaluates an expression against the execution, such as calling a method on a bean or reading a variable. No delegate class is created for the step, so there is little or no instance state to share. The risk moves elsewhere: expressions can grow into logic that is hard to read, version and test, and they can call methods with side effects that the model author does not see.
- Where state lives: the expression reads process variables and bean state, so the variables it touches are the real state.
- How it is shared: the expression itself holds no state, but any bean it calls behaves like a singleton.
- Side effects: keep expressions to reads and simple calls, and move anything that writes to external systems into a delegate.
- What to test: unit test the called bean directly, then run the model to confirm the expression resolves with the variable types it receives.
- Scripting: check the Flowable security documentation before allowing scripted expressions in models that others can edit.
Review each expression during model review, since a short expression in a diagram can hide a method call that changes shared data.
Delegate design options and when each fits
This table compares four ways to write a Flowable delegate, from stateless code that reads everything from execution variables to external workers that run outside the engine. Read each row for where state lives and how concurrent executions can collide, then confirm the engine behavior described against your Flowable version before standardizing on a pattern.
| Option | Where state lives | Concurrency risk | Testing approach | When it fits |
|---|---|---|---|---|
| Stateless delegate reading execution variables | Process variables and the execution, scoped to one instance | Low, because each execution supplies its own inputs | Invoke the delegate with a mocked execution and assert variable outputs | Logic that maps inputs to outputs and needs no external collaborator |
| Delegate holding injected stateless services | Read-only references to beans, with state kept in the database or variables | Low if the injected service itself is stateless and thread-safe | Inject test doubles for the service and verify calls and variable writes | Calls to APIs or repositories where the service keeps no per-request data |
| Delegate holding mutable fields | Instance fields that the engine may share across executions | High, because one instance can serve parallel executions and overwrite fields | Tests pass in isolation but may miss races, so add concurrent execution tests | Rarely appropriate. Move the data into variables or method-local scope instead |
| External worker | Outside the engine, in the worker's own process and memory | Managed by the worker and the broker, with retries and duplicate delivery to design for | Test the worker as ordinary code and test the message contract separately | Work in another language, long-running calls, or isolation from engine threads |
Reviewing and hardening delegates in your Flowable project
This sequence takes a Flowable project from an unknown delegate inventory to a set of delegates that keep execution data out of shared fields, behave correctly under concurrent and asynchronous execution, and pass tests that exercise those conditions. Each step ends with a check you can record as evidence for your team or auditors.
Inventory every delegate and listener
Start by listing every service task, script task, execution listener, task listener and event listener in your BPMN and CMMN models that points to a Java class or a Spring bean. Include delegateExpression and expression references, because a bean named in a model is easy to miss when you only search Java sources. Your developers can produce this list with a search over the BPMN and CMMN resources and the source tree, and a process owner should confirm that no model is missing from the list. Done means you have one table with the model, the activity ID, the reference type (class, delegateExpression or expression), and the target class or bean name.
Classify each delegate as stateless or stateful
For each entry in the inventory, read the class and decide whether it holds state between invocations. A delegate is stateful if it declares non-final instance fields that are set from a model (for example, field injection with a field declaration in the BPMN extension elements), if it caches execution data in a field, or if it keeps a collection that grows across calls. Mark any class that uses a field injection pattern as a priority for review, because Flowable can reuse a delegate instance across executions. The engineer who owns each delegate should sign off on its classification. Done means every row has a stateless or stateful label and a short reason.
Move execution data into variables and parameters
For each stateful delegate, rewrite the logic so that per-execution values come from the DelegateExecution passed to execute, or from process variables read with execution.getVariable, and results are written back with setVariable. Keep helper methods parameterized so that values flow through method arguments rather than fields. Remove field injection for data values, and keep only collaborators that are themselves thread-safe, such as configured clients or repositories. Done means no delegate writes execution-specific data to a non-final instance field, and a code review confirms that each changed delegate reads its inputs from the execution context.
Set Spring bean scope on purpose
Where a delegate is exposed as a Spring bean through delegateExpression or an expression, confirm its scope in the bean definition or annotation. Singleton scope is the usual default and means one instance serves all executions, so any remaining mutable state must be safe for concurrent use. Use a prototype scope only when each invocation genuinely needs its own instance, and be aware that this changes how dependencies are created. Check the Spring Boot starter in use and confirm the Flowable version you run against the Flowable Spring Boot documentation, since the starter and Spring Framework baseline determine the bean behavior you test. Done means each bean has a documented scope and its owner has confirmed the choice.
Add concurrency tests for shared delegates
Write tests that start many process instances in parallel against the same delegate and assert that each instance receives its own result. Use a fixed thread pool, a shared start latch so that threads begin together, and assertions on the variables each instance ends with. Also test a delegate that reaches a wait state, such as a user task or timer, so that data is read after the instance resumes. Unit tests with JUnit 5 cover single-instance behavior, but only concurrent runs expose shared-field defects. Done means the concurrency test fails against the original stateful version of at least one delegate you changed, and passes after the fix.
Verify behavior with the async executor enabled
Run the same scenarios against an engine configuration where the async executor is enabled and async continuation is set on the activities that matter. The async executor runs jobs from a thread pool and coordinates nodes through lock owners and lock expiration times in the database, so a delegate can execute on a different thread and after a transaction boundary. Confirm the behavior in the Flowable advanced documentation for your version, and check the configuration reference for the database you use. Done means the async scenarios pass, the job tables show completed jobs without errors, and no delegate leaves data behind between jobs.
Record the review and repeat it on upgrade
Save the inventory, classifications, changed delegates, scope decisions and test results together in your project repository so that the review is traceable. Repeat the classification step whenever a new delegate is added, and rerun the concurrency and async tests after an engine or Spring upgrade. Before you upgrade across a major version, check the release notes for the Flowable version you target, including the Spring Boot and Java baseline it requires. Done means the repository holds a dated review record and a test suite that runs in your build pipeline.
Security and operational considerations for shared delegates
A delegate that works correctly in a single-customer test can leak data or credentials once one instance serves many tenants, executions and log streams. The cards below cover the checks that connect shared delegate code to isolation, logging, credentials and day-two monitoring in your own environment.
Tenant data isolation in delegates
A delegate with an injected field or a cached value can carry one customer's data into another customer's execution if the same instance handles both. Check whether any field, static collection or bean scope holds per-execution or per-tenant values. Confirm that tenant identifiers come from the execution's variables or tenant ID on each call and are never stored on the delegate. Test with two tenants running concurrently in a staging environment before you accept the design.
Personal data in variables and logs
Process variables often hold names, account numbers or health details, and a delegate that logs its variable map sends that data to every log sink. Review each delegate's logging statements and exception messages, including the messages Flowable writes when a job fails. Confirm that your log pipeline masks or excludes the variable names that carry personal data, and that retention in the log store matches your data protection rules.
Credentials in injected clients
Clients for payment, CRM or messaging APIs are often injected into delegates with their credentials already configured. Check where those secrets are read from, whether they are visible in configuration files or deployment manifests, and whether a rotation changes the running client without a restart. Confirm that the service account behind each client has only the permissions that delegate needs. Keep the secret out of process variables, which the history tables and REST API can expose.
Actuator endpoint exposure
The Flowable Spring Boot starters expose an actuator endpoint at /actuator/flowable for monitoring running processes. Confirm which actuator endpoints are exposed on each network, who can reach them, and whether they are authenticated. Decide which operational metrics your team reads from this endpoint and which alerts depend on them, so that a stuck delegate shows up before users report it.
Failed job review and retries
When a delegate throws an exception, the async executor records a failed job and retries it, so a bug can repeat silently across many instances. Set a regular review of failed jobs with the owner of each delegate, and agree who may retry, suspend or delete them. Confirm that job review does not expose the variables of affected customers to staff who are not authorized to see them. Track the rate of new failures after each release.
Lock behavior across nodes
The async executor coordinates multiple nodes through lock owners and lock expiration times stored in the database, so a job can move to another node when a lock expires. A delegate that calls an external system without an idempotency key may act twice when this happens. Check whether each external call can be repeated safely, and confirm the lock expiration settings in your configuration match the longest normal run of your delegates.
Questions teams ask about Flowable delegate thread safety
Does making a Flowable delegate stateless fix every thread safety problem?
No. A stateless delegate removes one class of shared-state bug, the kind caused by instance fields that hold per-execution data, but it does not make the code around it safe. Shared resources such as static caches, singleton Spring beans with mutable fields, non-thread-safe formatters or clients, and external calls that are not idempotent can still produce wrong results when two executions run at once. Statelessness is the starting point for the review, not the end of it. Ask the reviewer to check every field, every static member and every bean the delegate calls.
How much effort does a Flowable delegate review take, and what drives it?
The effort depends mainly on how many delegates and listeners exist, how many of them hold fields or call shared beans, and how much of the engine's concurrency your deployment actually uses. A small process application with a handful of delegates is a short review. A large estate with many shared Spring beans, custom listeners and external integrations takes longer, because each dependency needs to be traced. Ask any partner for a quote built from the delegate inventory, the list of shared dependencies, and the concurrency tests you expect to write. Do not accept a figure that is not tied to those inputs.
Can our own team review Flowable delegates for thread safety?
Yes, if engineers know the Java memory model, how the engine creates delegate instances, and how Spring scopes beans. The review is a code reading exercise followed by targeted tests, so experienced Java developers with access to the Flowable documentation can do it. The gap usually appears in concurrency testing: reproducing a race between two process instances that touch the same bean requires load or parallel test design that many teams have not built. Decide early who owns that test harness, and whether the team will keep it running after the review ends.
What does the work need from engineers and QA?
Engineers need to read the delegate classes, the Spring bean definitions and the BPMN service tasks that reference them. They also need to map which delegates run inside the async executor, because those run on a thread pool and can overlap. QA needs repeatable concurrency tests that start many process instances against the same definition and check the resulting variables, history and external side effects. Plan time for test data, a database that matches production, and a way to reset state between runs. Without these inputs, a review can only confirm the code looks correct.
How do Flowable 8 upgrades affect delegate code?
Flowable 8.0.0 was released on February 27, 2026, and its release notes describe an upgrade to Spring Framework 7 and Spring Boot 4 with no backward compatibility for Spring Boot 3. It also makes Jackson 3 the default JSON library, with a Jackson 2 compatibility layer, and removes JUnit 3 and 4 support. For delegates, the practical risks are changes in how beans are wired, how JSON variables are serialized, and how tests are written. Re-run the thread safety review after the upgrade, because a serialization or bean scope change can alter what is shared. Confirm each point against the release notes for your target version.
When should we bring in outside help for delegate thread safety?
Bring in outside help when the delegate estate is large, when the processes handle regulated or financial data where a race condition would be hard to explain to an auditor, or when the team lacks someone who has built concurrency tests for a Java workflow engine. An external review is also useful before a major upgrade or a move to a different deployment topology, such as more nodes sharing one database. DeployOpen works on fixed-scope engagements with the scope, deliverables and timeline agreed in writing before work starts. Ask for the review method, the test cases it will produce and what remains with your team afterwards.
Will a delegate review certify that our Flowable deployment is compliant?
No. A thread safety review produces findings, fixes and tests that support your controls, but it does not produce an audit report or certification. DeployOpen prepares systems, controls and evidence for compliance assessments, and independent auditors decide whether the evidence meets the standard. Ask the reviewer to document which delegates were reviewed, what tests ran, which findings were closed and who accepted the residual risk. That record is the part an assessor is most likely to ask about.
How DeployOpen reviews and hardens Flowable delegates
DeployOpen reviews the Java delegates, expressions and Spring beans in your Flowable models for shared-state risk, then fixes them in your codebase within a written scope. The work runs inside your own infrastructure and repositories, and the result is documented so your team can maintain it after handover.
How we scope
- Inventory every service task, execution listener and delegate class that your BPMN and CMMN models reference.
- Record which delegates hold injected fields, static state or caches that outlive a single execution.
- Agree in writing which delegates are in scope, the target patterns, and the test and handover deliverables before work starts.
- Engagements are fixed-scope, and ongoing review of new delegates can be covered by a retainer.
How we build
- Refactor stateful delegates to keep per-execution data in process variables or method-local objects.
- Replace field-injected collaborators with expressions or Spring beans whose scope and thread safety are documented.
- Add concurrency tests that run the same delegate across parallel executions and check that variables do not leak.
How we hand over
- Deliver the changed source, the test suite and a short register of delegates with their scope and state rules.
- Document the coding conventions for new delegates so reviewers can check injected fields during code review.
- Walk your engineers through the changes and confirm which decisions they will own after the engagement closes.
How we support
- Review new or changed delegates against the documented conventions when your team requests it under a retainer.
- Revisit delegate code when you upgrade Flowable or change Spring versions, since bean scope and injection behavior can shift.
- Confirm with your team which Flowable version and Spring baseline your delegates target before any review begins.

