Hi Allard,
as you might have noticed Nils is my colleague and we are currently working in the same project. I created a reproducable example at GitHub: https://github.com/OLibutzki/axon-hierarchical/tree/state-stored-aggregate-transaction
Please check out the state-stored-aggregate-transaction branch.
There are two modules (module1 and module2). Each of them represents a bounded context. module2 has a state-stored aggregate called Module2Aggregate. The creation of this aggregate is triggered by the CreateModule2AggregateCommand which is sent by the Module1CommandSender located in module1.
Both, module1 and module2, have a seperate Spring application context which are children of the root context (see AxonHierarchicalApplication class). The third child module (called axon) currently provided the infrasturcture part (ComandBus, CommandGateway, EventBus,…). These beans are exported (by the ExportedBeanPostProcessor) to the root context and can therefore be used from the other child contexts.
Notice: Each module (axon, module1, module2) does have a seperate datasource and a seperate JPA transaction manager. They do it this way in order to decouple the persistence of the modules as much as possible. This enables the possibility to decide at any point of time to run those modules in one application (single deployment unit) or in a distributed way.
What happens when you run the sample application:
-
de.libutzki.axon.axonhierarchical.module1.Module1CommandSender.sendCommand() is invoked by de.libutzki.axon.axonhierarchical.AxonHierarchicalApplication.main(String[])
-
Because of the @Transactional annotation a new transaction is started by the module1 transaction manager.
-
Module1CommandSender sends the CreateModule2AggregateCommand via the CommandGateway which is provided (exported to the root context) by the axon module.
-
The CommandGateway delegates the command to the SimpleCommandBus which is initialized the the transaction manager of the axon module.
-
A transaction is started by the axon transaction manager
-
The CommandHandler de.libutzki.axon.axonhierarchical.module2.aggregate.Module2Aggregate.Module2Aggregate(CreateModule2AggregateCommand) is invoked,
-
The method invocation works, but after that an Exception is thrown:
javax.persistence.TransactionRequiredException: no transaction is in progress
(Full stacktrace attached at the end)
It’s obvious why this exception is thrown: The axon transaction manager started the transaction, but we expect the module2 transaction manager to start the transaction.
As the CommandBus is bound to a single transaction manager we think about having a command bus in each child module. Every command bus is bound to the child module’s transaction manager.
When having multiple command buses, we need to introduce a central command bus which orchestrates all the child command buses concerning handler subscriptions and message dispatching. We think it’s similar to what a DistributedCommandBus does. Anyway, it might not be the right choice to reuse the DistributedCommandBus as concepts like the load factor are not relevant for us. So having a new command bus implementation for orchestrating our independent child command buses might be the best solution.
I hope you got our point. If you have any suggestion how to solve this, we are very keen to know.
Thanks for your support!
Kind regards
Oliver
Stacktrace:
`
Exception in thread “main” javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:398)
at org.hibernate.internal.SessionImpl.checkTransactionNeededForUpdateOperation(SessionImpl.java:3550)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1443)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1439)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:350)
at com.sun.proxy.$Proxy70.flush(Unknown Source)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:308)
at com.sun.proxy.$Proxy70.flush(Unknown Source)
at org.axonframework.modelling.command.GenericJpaRepository.doSaveWithLock(GenericJpaRepository.java:145)
at org.axonframework.modelling.command.GenericJpaRepository.doSaveWithLock(GenericJpaRepository.java:53)
at org.axonframework.modelling.command.LockingRepository.doSave(LockingRepository.java:149)
at org.axonframework.modelling.command.LockingRepository.doSave(LockingRepository.java:52)
at org.axonframework.modelling.command.AbstractRepository.doCommit(AbstractRepository.java:194)
at org.axonframework.modelling.command.AbstractRepository.prepareForCommit(AbstractRepository.java:180)
at org.axonframework.modelling.command.LockingRepository.prepareForCommit(LockingRepository.java:131)
at org.axonframework.modelling.command.LockingRepository.prepareForCommit(LockingRepository.java:52)
at org.axonframework.modelling.command.AbstractRepository.lambda$newInstance$0(AbstractRepository.java:86)
at org.axonframework.messaging.unitofwork.MessageProcessingContext.notifyHandlers(MessageProcessingContext.java:71)
at org.axonframework.messaging.unitofwork.DefaultUnitOfWork.notifyHandlers(DefaultUnitOfWork.java:106)
at org.axonframework.messaging.unitofwork.AbstractUnitOfWork.changePhase(AbstractUnitOfWork.java:222)
at org.axonframework.messaging.unitofwork.AbstractUnitOfWork.commitAsRoot(AbstractUnitOfWork.java:83)
at org.axonframework.messaging.unitofwork.AbstractUnitOfWork.commit(AbstractUnitOfWork.java:71)
at org.axonframework.messaging.unitofwork.DefaultUnitOfWork.executeWithResult(DefaultUnitOfWork.java:92)
at org.axonframework.commandhandling.SimpleCommandBus.handle(SimpleCommandBus.java:176)
at org.axonframework.commandhandling.SimpleCommandBus.doDispatch(SimpleCommandBus.java:141)
at org.axonframework.commandhandling.SimpleCommandBus.dispatch(SimpleCommandBus.java:110)
at org.axonframework.commandhandling.gateway.AbstractCommandGateway.send(AbstractCommandGateway.java:75)
at org.axonframework.commandhandling.gateway.DefaultCommandGateway.send(DefaultCommandGateway.java:73)
at org.axonframework.commandhandling.gateway.DefaultCommandGateway.sendAndWait(DefaultCommandGateway.java:90)
at de.libutzki.axon.axonhierarchical.module1.Module1CommandSender.sendCommand(Module1CommandSender.java:27)
at de.libutzki.axon.axonhierarchical.module1.Module1CommandSender$$FastClassBySpringCGLIB$$287801a8.invoke()
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at de.libutzki.axon.axonhierarchical.module1.Module1CommandSender$$EnhancerBySpringCGLIB$$683db6d8.sendCommand()
at de.libutzki.axon.axonhierarchical.AxonHierarchicalApplication.main(AxonHierarchicalApplication.java:24)
`