When using the embedded adapter, any engine interaction — starting a process, correlating a message to a running instance, or sending a signal — inside a @Transactional method does not participate in that transaction. If the transaction rolls back, the engine write is already committed — leaving the system in an inconsistent state where the engine has advanced but the corresponding business object was never persisted.
Same issue as bpm-crafters/process-engine-adapters-cib-seven#30 — tracked separately per adapter.
Steps to reproduce
- Library version: (current)
- JDK version: 17
- Operating system: any
- Complete executable reproducer: see inline code below
- Steps:
- Use the embedded adapter in a Spring Boot application (engine and application share the same database).
- Inside a single
@Transactional service method: save a domain object and perform any engine interaction (start a process, correlate a message, send a signal).
- Let the method fail (exception) after the engine call.
@Transactional
fun createOrder(): Order {
val saved = orderRepository.save(Order.createNew())
orderProcessPort.orderCreated(saved.id) // any engine call: start, correlate, signal
throw IllegalStateException("Simulated failure — expect full rollback")
}
Expected behaviour
Because the application and engine share one database, both writes — the domain object and the engine state change — should participate in the same transaction. If the transaction rolls back, neither write is persisted. This all-or-nothing guarantee is one of the main reasons to run an embedded engine in the first place.
Actual behaviour
Only the domain write is rolled back. The engine write is already committed and takes effect regardless. Depending on where in the process lifecycle the call happens this can mean:
- a process instance is started for a business object that does not exist,
- a message correlation advances a running process whose triggering business action was never saved,
- a signal moves the process forward while the application-side state is gone.
In all cases the process and the domain model are now out of sync. Any subsequent engine activity (user tasks, timers, service calls) will operate on missing or stale data and likely cause further errors downstream. In short: what should be a clean rollback turns into silent data corruption.
Technical root cause: all affected API implementations wrap the engine call in CompletableFuture.supplyAsync { } without an explicit Executor, which causes it to run on a separate thread (ForkJoinPool.commonPool()). Spring's transaction context is thread-bound (via ThreadLocal) and is therefore not visible to that thread. The engine call opens and commits its own independent transaction, bypassing the caller's rollback. Per the CompletableFuture javadoc, the no-executor overload always uses ForkJoinPool.commonPool().
// e.g. StartProcessApiImpl.kt, StartProcessByMessageCmd branch
CompletableFuture.supplyAsync { // ← no Executor → runs on ForkJoinPool, outside the caller's transaction
runtimeService
.createMessageCorrelation(cmd.messageName)
.setVariables(payload)
.correlateStartMessage()
.toProcessInformation()
}
Affected classes in c7-embedded-core:
StartProcessApiImpl — all startProcess variants (lines 27, 56)
CorrelationApiImpl.correlateMessage (line 28)
SignalApiImpl.sendSignal (line 25)
When using the embedded adapter, any engine interaction — starting a process, correlating a message to a running instance, or sending a signal — inside a
@Transactionalmethod does not participate in that transaction. If the transaction rolls back, the engine write is already committed — leaving the system in an inconsistent state where the engine has advanced but the corresponding business object was never persisted.Same issue as bpm-crafters/process-engine-adapters-cib-seven#30 — tracked separately per adapter.
Steps to reproduce
@Transactionalservice method: save a domain object and perform any engine interaction (start a process, correlate a message, send a signal).Expected behaviour
Because the application and engine share one database, both writes — the domain object and the engine state change — should participate in the same transaction. If the transaction rolls back, neither write is persisted. This all-or-nothing guarantee is one of the main reasons to run an embedded engine in the first place.
Actual behaviour
Only the domain write is rolled back. The engine write is already committed and takes effect regardless. Depending on where in the process lifecycle the call happens this can mean:
In all cases the process and the domain model are now out of sync. Any subsequent engine activity (user tasks, timers, service calls) will operate on missing or stale data and likely cause further errors downstream. In short: what should be a clean rollback turns into silent data corruption.
Technical root cause: all affected API implementations wrap the engine call in
CompletableFuture.supplyAsync { }without an explicitExecutor, which causes it to run on a separate thread (ForkJoinPool.commonPool()). Spring's transaction context is thread-bound (viaThreadLocal) and is therefore not visible to that thread. The engine call opens and commits its own independent transaction, bypassing the caller's rollback. Per the CompletableFuture javadoc, the no-executor overload always usesForkJoinPool.commonPool().Affected classes in
c7-embedded-core:StartProcessApiImpl— allstartProcessvariants (lines 27, 56)CorrelationApiImpl.correlateMessage(line 28)SignalApiImpl.sendSignal(line 25)