Thread safety in Flowable Java delegates

Flowable Java delegates can hold state that leaks between process executions if they are written carelessly. This page explains how delegate instances and field injection behave and which patterns keep execution data isolated.

On this page

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.

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.

OptionWhere state livesConcurrency riskTesting approachWhen it fits
Stateless delegate reading execution variablesProcess variables and the execution, scoped to one instanceLow, because each execution supplies its own inputsInvoke the delegate with a mocked execution and assert variable outputsLogic that maps inputs to outputs and needs no external collaborator
Delegate holding injected stateless servicesRead-only references to beans, with state kept in the database or variablesLow if the injected service itself is stateless and thread-safeInject test doubles for the service and verify calls and variable writesCalls to APIs or repositories where the service keeps no per-request data
Delegate holding mutable fieldsInstance fields that the engine may share across executionsHigh, because one instance can serve parallel executions and overwrite fieldsTests pass in isolation but may miss races, so add concurrent execution testsRarely appropriate. Move the data into variables or method-local scope instead
External workerOutside the engine, in the worker's own process and memoryManaged by the worker and the broker, with retries and duplicate delivery to design forTest the worker as ordinary code and test the message contract separatelyWork in another language, long-running calls, or isolation from engine threads

Shared-state bugs that appear in production

Delegates run on engine threads, get reused across executions, and sit in long-lived Spring or servlet contexts. That combination lets state written for one process instance leak into another. The cards below describe failure patterns that tend to pass unit tests and surface under concurrent load, with the review step that catches each one.

Mutable fields holding process data

A delegate declares a field such as private String customerId and sets it from a field expression or from a method call during execution. Flowable creates the delegate once per class or expression, so two concurrent executions can overwrite each other's value before the write is read. Illustrative case: two approvals run in parallel, and one request is sent with the other customer's identifier. Review step: search delegate classes for non-final instance fields and confirm each one is either a configured constant or a bean dependency, never per-execution data.

Caches keyed without the instance

A delegate caches a lookup result in a map keyed only by a business value that is not unique across process instances, such as a status code or a product type. The first execution fills the cache and later instances read a stale or foreign result. Illustrative case: a tax rate cached by region keeps serving a rate that was valid before a rule change. Review step: check every cache key includes the version or input that actually determines the result, and that entries have an expiry or are invalidated when deployments change.

Thread-local values left behind

Code stores the current tenant, user or correlation ID in a ThreadLocal and never clears it. Flowable runs jobs on pooled threads from the async executor, so the next job on that thread can inherit the previous value. Illustrative case: a timer job runs under the identity of a user who started an unrelated process. Review step: confirm every ThreadLocal.set has a matching remove in a finally block, and test two sequential jobs on the same pool thread to show the second one sees a clean context.

Clients reused across calls unsafely

A delegate holds an HTTP client, SDK object or date formatter as a field and reuses it, and that object is not safe for concurrent calls or carries per-request headers and state. Requests can interleave, and a header set for one call can appear on another. Illustrative case: a SimpleDateFormat shared across threads produces wrong timestamps under load. Review step: check each third-party client class for documented thread-safety, and require either a shared, thread-safe client configured once or a new instance per call.

Static collections accumulating state

A static list or map collects results, audit entries or retry counters across executions. It grows for the life of the JVM, can be read by concurrent executions without synchronization, and is lost on restart, so the state never matches the process history. Illustrative case: a static list of 'pending notifications' keeps entries from completed instances and sends duplicates after a node restarts. Review step: search for static collections in process code, and move any state that must survive a restart into process variables, the database or an external store.

Spring singletons with request scope

A Spring bean used by delegates is a singleton, yet it stores fields set during one execution, such as a current user or the active variable map. Every process instance calling the bean shares that one object. Illustrative case: a singleton ApprovalContext bean keeps the last approver's identity and writes it into the next instance's audit record. Review step: confirm beans called from delegates are stateless, pass data as method arguments, and that any scoped bean is tested with two overlapping executions.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.