Skip to content

feat(embedded): introduce EngineCommandExecutor for pluggable command dispatch#157

Merged
zambrovski merged 1 commit intodevelopfrom
fix/issue-156
Feb 24, 2026
Merged

feat(embedded): introduce EngineCommandExecutor for pluggable command dispatch#157
zambrovski merged 1 commit intodevelopfrom
fix/issue-156

Conversation

@emaarco
Copy link
Member

@emaarco emaarco commented Feb 18, 2026

Overview

Further steps

  • one could think about adding this EngineCommandExecutor to the process-engine-api root as well (providing it to all adapters centraly)

@emaarco emaarco added the Type: bug Something isn't working label Feb 18, 2026
@emaarco emaarco requested a review from zambrovski February 18, 2026 08:24
@emaarco emaarco self-assigned this Feb 18, 2026
Introduce EngineCommandExecutor to centralise how the four core engine
calls (correlateMessage, sendSignal, startProcess, deploy) are dispatched.

The default executor submits work to ForkJoinPool.commonPool(), mirroring
the behaviour of a remote engine and keeping adapters portable across
engine implementations (embedded ↔ remote, Camunda 7 ↔ Zeebe).

Users override the Spring bean with any java.util.concurrent.Executor to
change the dispatch strategy — most commonly a same-thread executor for
@transactional coupling.

Closes #156
@emaarco emaarco changed the title fix(embedded): run engine calls synchronously to honour @Transactional feat(embedded): introduce EngineCommandExecutor for pluggable command dispatch Feb 18, 2026
Comment on lines +24 to +26
class EngineCommandExecutor(
private val executor: Executor = ForkJoinPool.commonPool()
) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, would it be sufficient to not have an EngineCommandExecutor, and just inject a "same thread executor" bean where needed and pass it to the supplyAsync calls there? Should be the same effect but with one fewer class.

Copy link
Member Author

@emaarco emaarco Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@i-am-not-giving-my-name-to-a-machine - if i understand you right, this should work as well, yes. And was also my first suggestion (to just execute the operation sync, and then wrap the result in a CompletableFuture.completedFuture(...) or as an alternative use a sync executor right away)

However, @zambrovski's take on this to make it customizable. So that the default behaviour is still async, and you need to opt in to sync execution with a "same thread executor". So that you do not become coupled to the embedded engine through implicit behavior of the adapter—but rather the application remains portable.

Thus i'd suggested this solution (which would also open the door to set another pool than the default forkJoinPool of supplyAsync, if a user would for example like to use something like newVirtualThreadPerTaskExecutor for whatever reason)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm with @zambrovski on this, that it should be customizable and the default is what the API implies: async. However, I really like the option for transactional engine commands. I'd merely introduce an Executor bean (that is specifically for the service implementations that you modified). All I'm suggesting is to replace

  @Bean
  @ConditionalOnMissingBean
  fun engineCommandExecutor(): EngineCommandExecutor = EngineCommandExecutor()

with

  @Bean
  @ConditionalOnMissingBean
  fun engineCommandExecutor(): Executor = ForkJoinPool.commonPool()

and delete EngineCommandExecutor. Unless this would replace something in Spring and it would really hurt everyone who uses this lib, because this change should not interfere with anything else of course.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, got it :) not sure about that either, whether there is some kind of default bean on the Executor class.
Therefore i implemented it as a property of another class.

To make it

  • a) more explicit what the bean is about (just relevant to sending commands to the engine)
  • b) prevent issues if a user/spring itself already defined another Executor bean, thats used somewhere else.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if an indirection is necessary, so be it.

Tbh, Spring beans are not my strong suit, my background is in Jakarta EE and way back stuff, but I'd expect a mechanism in Spring that enables arbitrary beans of the same type that don't interfere with any default beans.

Copy link
Member Author

@emaarco emaarco Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use beans with specific qualifiers/names throughout the whole adapter, I expect this should be possible in Spring without bigger problems.

But given that, I don't see an outweighting advantage in this approach, since i force the user to give the bean a specific name - and neither a big disadvantage in defining one class that represents a lot better what the executor is about. Think it shouldn’t be that expressive to maintain in the long run - also considering that it could be somehow shared since all embedded adapters could require it

But i can live with both

@zambrovski zambrovski merged commit 8ad9172 into develop Feb 24, 2026
2 checks passed
@zambrovski zambrovski deleted the fix/issue-156 branch February 24, 2026 19:02
@zambrovski zambrovski added this to the 2026.02.1 milestone Feb 24, 2026
@zambrovski
Copy link
Contributor

I think the implementation is good enough with both approaches, but since @emaarco provided the PR, let his implementation remain there until we find a sound reason why the indirection is bad and a default executor should be exposed as a Spring Bean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Embedded adapter: process state and domain state can diverge on rollback

3 participants