Thanks Ivan. This is very helpful.
I’d like to run some thoughts by you on this. What you described is a fully event-driven system. I think a consequence of this design is that the steps required to satisfy various use cases becomes “hidden”.
It takes time for a developer to follow the codepath from event to event to get the full picture to see all the steps required to satisfy any given use case. You can’t rely on IntelliJ call hierarchy code paths because of the decoupled nature of event systems.
Thus it becomes harder for an engineer to say that the code successfully satisfies all the steps required to fulfill any given use case because that code is distributed throughout the codebase.
As an alternative, we’ve formalized the notion of a UseCase (or Workflow). It’s essentially similar to
Function<T,R> but the
apply() method is reactive. The UseCase coordinates the all steps required to satisfy the use case. Using reactive APIs (Project Reactor) and Kotlin coroutines, we can ensure none of the code is blocking, which gives a similar benefit to the fully event driven system, but it also brings all the code into a single location so that engineers can see in one place all the steps required to satisfy that use case.
Each step in that use case is a function that follows the Single Responsibility Principle. So, it’s a bit like assembling legos. You don’t have to worry about side effects because the “step functions” only do what the function says it does, nothing else.
With Axon, the plan is combine the approach you outlined in your last response with the UseCase pattern to try and get the best of the event-driven system, while also making the code required to fulfill a use case explicit.