Circular reference error while trying to add command dispatch intercpetors

Hi.

I have a Kotlin project that uses the Axon Server as the command bus and I’m trying to add command dispatch interceptors to the existing command bus.

This is my config file:

@Configuration
class AxonConfig {
    @Bean
    fun trackingEventProcessorConfiguration(): TrackingEventProcessorConfiguration =
        TrackingEventProcessorConfiguration
            .forSingleThreadedProcessing()
            .andInitialTrackingToken { obj: StreamableMessageSource<TrackedEventMessage<*>?> -> obj.createHeadToken() }

    @Bean
    fun configurer(
        configurer: Configurer,
        addNewTagToNewCategoryCommandInterceptor: AddNewTagToNewCategoryCommandInterceptor,
        addNewTagToExistingCategoryCommandInterceptor: AddNewTagToExistingCategoryCommandInterceptor,
    ): Configurer {
        // Configure event processing
        configurer.eventProcessing { eventProcessingConfigurer ->
            // Enable tracking event processors globally
            eventProcessingConfigurer.usingTrackingEventProcessors()
        }

        // Configure the command bus
        configurer.configureCommandBus { configuration ->
            val commandBusWithInterceptors = configuration.commandBus()

            // Register dispatch interceptors directly on the provided commandBus instance
            commandBusWithInterceptors.registerDispatchInterceptor(addNewTagToNewCategoryCommandInterceptor)
            commandBusWithInterceptors.registerDispatchInterceptor(addNewTagToExistingCategoryCommandInterceptor)

            commandBusWithInterceptors // Return the customized CommandBus
        }

        return configurer
    }
}

But this causes a stack overflow error:

ERROR 35852 --- [walletaccountant] [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'accountAggregateRepository': Failed to instantiate [org.axonframework.modelling.command.Repository]: Factory method 'repository' threw exception with message: null
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:648) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:636) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1355) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1185) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:971) ~[spring-context-6.1.14.jar:6.1.14]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:625) ~[spring-context-6.1.14.jar:6.1.14]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.3.5.jar:3.3.5]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.3.5.jar:3.3.5]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.3.5.jar:3.3.5]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.3.5.jar:3.3.5]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.3.5.jar:3.3.5]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.3.5.jar:3.3.5]
	at com.wa.walletaccountant.WalletAccountantApplicationKt.main(WalletAccountantApplication.kt:13) ~[main/:na]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.axonframework.modelling.command.Repository]: Factory method 'repository' threw exception with message: null
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:178) ~[spring-beans-6.1.14.jar:6.1.14]
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:644) ~[spring-beans-6.1.14.jar:6.1.14]
	... 19 common frames omitted
Caused by: java.lang.StackOverflowError: null
	at java.base/java.lang.invoke.DirectMethodHandle.allocateInstance(DirectMethodHandle.java:501) ~[na:na]
	at org.axonframework.config.DefaultConfigurer$ConfigurationImpl.getComponent(DefaultConfigurer.java:1127) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.getComponent(Configuration.java:278) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.commandBus(Configuration.java:128) ~[axon-configuration-4.10.1.jar:4.10.1]
	at com.wa.walletaccountant.common.config.AxonConfig.configurer$lambda$2(AxonConfig.kt:34) ~[main/:na]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.DefaultConfigurer$ConfigurationImpl.getComponent(DefaultConfigurer.java:1132) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.getComponent(Configuration.java:278) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.commandBus(Configuration.java:128) ~[axon-configuration-4.10.1.jar:4.10.1]
	at com.wa.walletaccountant.common.config.AxonConfig.configurer$lambda$2(AxonConfig.kt:34) ~[main/:na]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.DefaultConfigurer$ConfigurationImpl.getComponent(DefaultConfigurer.java:1132) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.getComponent(Configuration.java:278) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.commandBus(Configuration.java:128) ~[axon-configuration-4.10.1.jar:4.10.1]
	at com.wa.walletaccountant.common.config.AxonConfig.configurer$lambda$2(AxonConfig.kt:34) ~[main/:na]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.DefaultConfigurer$ConfigurationImpl.getComponent(DefaultConfigurer.java:1132) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.getComponent(Configuration.java:278) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.commandBus(Configuration.java:128) ~[axon-configuration-4.10.1.jar:4.10.1]
	at com.wa.walletaccountant.common.config.AxonConfig.configurer$lambda$2(AxonConfig.kt:34) ~[main/:na]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.DefaultConfigurer$ConfigurationImpl.getComponent(DefaultConfigurer.java:1132) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.getComponent(Configuration.java:278) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.commandBus(Configuration.java:128) ~[axon-configuration-4.10.1.jar:4.10.1]
	at com.wa.walletaccountant.common.config.AxonConfig.configurer$lambda$2(AxonConfig.kt:34) ~[main/:na]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.DefaultConfigurer$ConfigurationImpl.getComponent(DefaultConfigurer.java:1132) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.getComponent(Configuration.java:278) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.commandBus(Configuration.java:128) ~[axon-configuration-4.10.1.jar:4.10.1]
	at com.wa.walletaccountant.common.config.AxonConfig.configurer$lambda$2(AxonConfig.kt:34) ~[main/:na]
	at org.axonframework.config.Component.get(Component.java:85) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.DefaultConfigurer$ConfigurationImpl.getComponent(DefaultConfigurer.java:1132) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.getComponent(Configuration.java:278) ~[axon-configuration-4.10.1.jar:4.10.1]
	at org.axonframework.config.Configuration.commandBus(Configuration.java:128) ~[axon-configuration-4.10.1.jar:4.10.1]
	at com.wa.walletaccountant.common.config.AxonConfig.configurer$lambda$2(AxonConfig.kt:34) ~[main/:na]

Any ideas?

Hi Miguel,

the problem is that you have defined a bean of type Configurer that also has a dependency on a bean typed “Configurer”.
Try defining your bean type as ConfigurerModule. That allows you to remove the dependency on a Configurer bean. The ConfigurerModule is practically a Consumer. Axon will invoke this function at the right time to avoid circular dependencies.

After some “learning how to do it in Kotlin” time, I tried this:

@Bean
    fun eventProcessingConfigurerModule(): ConfigurerModule =
        ConfigurerModule { configurer: Configurer ->
            configurer.onInitialize { config: org.axonframework.config.Configuration ->
                // Configure event processing
                configurer.eventProcessing { eventProcessingConfigurer ->
                    // Enable tracking event processors globally
                    eventProcessingConfigurer.usingTrackingEventProcessors()
                }
            }
        }

    @Bean
    fun commandBusConfigurerModule(
        addNewTagToNewCategoryCommandInterceptor: AddNewTagToNewCategoryCommandInterceptor,
        addNewTagToExistingCategoryCommandInterceptor: AddNewTagToExistingCategoryCommandInterceptor,
    ): ConfigurerModule =
        ConfigurerModule { configurer: Configurer ->
            configurer.onInitialize { config: org.axonframework.config.Configuration ->
                // Configure the command bus
                configurer.configureCommandBus { configuration ->
                    val commandBusWithInterceptors = configuration.commandBus()

                    // Register dispatch interceptors directly on the provided commandBus instance
                    commandBusWithInterceptors.registerDispatchInterceptor(addNewTagToNewCategoryCommandInterceptor)
                    commandBusWithInterceptors.registerDispatchInterceptor(
                        addNewTagToExistingCategoryCommandInterceptor,
                    )

                    commandBusWithInterceptors // Return the customized CommandBus
                }
            }
        }

But am still getting the same issue, not sure why now. :slight_smile:

Change my implementation to:

@Bean
    fun commandBusConfigurerModule(
        addNewTagToNewCategoryCommandInterceptor: AddNewTagToNewCategoryCommandInterceptor,
        addNewTagToExistingCategoryCommandInterceptor: AddNewTagToExistingCategoryCommandInterceptor,
    ): ConfigurerModule =
        ConfigurerModule { configurer: Configurer ->
            configurer.onInitialize { config: org.axonframework.config.Configuration ->
                // Configure the command bus
                val commandBus = config.commandBus()
                // Register dispatch interceptors directly on the provided commandBus instance
                commandBus.registerDispatchInterceptor(addNewTagToNewCategoryCommandInterceptor)
                commandBus.registerDispatchInterceptor(addNewTagToExistingCategoryCommandInterceptor)
            }
        }

And now I’m getting:

ERROR 65326 --- [walletaccountant] [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

   accountController defined in file [/Users/mlouromanso/development/WalletAccountant/wallet-accountant/build/classes/kotlin/main/com/wa/walletaccountant/adapter/in/web/AccountController.class]
      ↓
   commandGateway defined in class path resource [org/axonframework/springboot/autoconfig/AxonAutoConfiguration.class]
┌─────┐
|  axonServerCommandBus defined in class path resource [org/axonframework/springboot/autoconfig/AxonServerBusAutoConfiguration.class]
↑     ↓
|  commandBus defined in class path resource [org/axonframework/springboot/autoconfig/AxonAutoConfiguration.class]
↑     ↓
|  springAxonConfiguration
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.


This seems similar to an issue I ran into as well. You can see my “solution” here. It was to add an @Lazy annotation to the classes that depended on the command bus.
Not sure if this applies for Kotlin - sorry :upside_down_face:, but hopefully this points you in the right direction. I’m not too thrilled with the solution for my implementation, but will hopefully get your app up and running.

Hey Alex.

Your suggestion, although not pretty, works.

But it seems integration test are also broken, not sure how @Lazy would help there. :slight_smile:

Still trying to figure it out.

As you can see on my master branch I’m still auto-wiring the interceptors. Not sure if this will fix your issue with integration tests, and as @Steven_van_Beelen mentioned in the post I previously linked, you’d be running the risk of Spring messing up the ordering. Just thought I’d mention it in case it gets you to a stable spot for now with your tests.

    @Autowired
    public void registerAccountCommandInterceptors(ApplicationContext context, CommandBus commandBus) {
        commandBus.registerDispatchInterceptor(
                context.getBean(ValidateCommandInterceptor.class)
        );
        commandBus.registerDispatchInterceptor(
                context.getBean(AccountCommandsInterceptor.class)
        );
        commandBus.registerDispatchInterceptor(
                context.getBean(BusinessCommandsInterceptor.class)
        );
        commandBus.registerDispatchInterceptor(
                context.getBean(LoyaltyBankCommandsInterceptor.class)
        );
        commandBus.registerDispatchInterceptor(
                context.getBean(TransactionCommandsInterceptor.class)
        );
    }

If you’re just trying to register interceptors, you should be able to do that like this:

@Autowired
public void registerInterceptors(CommandBus commandBus, MyInterceptor interceptor) {
    commandBus.registerDispatchInterceptor(interceptor);
}

That seemed to do the trick.

My final setup:

import org.axonframework.config.Configurer
import org.axonframework.config.ConfigurerModule
import org.axonframework.eventhandling.TrackedEventMessage
import org.axonframework.eventhandling.TrackingEventProcessorConfiguration
import org.axonframework.messaging.StreamableMessageSource
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class AxonConfig {
    @Bean
    fun trackingEventProcessorConfiguration(): TrackingEventProcessorConfiguration =
        TrackingEventProcessorConfiguration
            .forSingleThreadedProcessing()
            .andInitialTrackingToken { obj: StreamableMessageSource<TrackedEventMessage<*>?> -> obj.createHeadToken() }

    @Bean
    fun eventProcessingConfigurerModule(): ConfigurerModule =
        ConfigurerModule { configurer: Configurer ->
            // Configure event processing
            configurer.eventProcessing { eventProcessingConfigurer ->
                // Enable tracking event processors globally
                eventProcessingConfigurer.usingTrackingEventProcessors()
            }
        }
}

And

import com.wa.walletaccountant.application.interceptor.command.tagcategory.AddNewTagToExistingCategoryCommandInterceptor
import com.wa.walletaccountant.application.interceptor.command.tagcategory.AddNewTagToNewCategoryCommandInterceptor
import org.axonframework.commandhandling.CommandBus
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Configuration

@Configuration
class CommandBusConfig {
    @Autowired
    fun registerInterceptors(
        commandBus: CommandBus,
        addNewTagToNewCategoryCommandInterceptor: AddNewTagToNewCategoryCommandInterceptor,
        addNewTagToExistingCategoryCommandInterceptor: AddNewTagToExistingCategoryCommandInterceptor,
    ) {
        commandBus.registerDispatchInterceptor(addNewTagToNewCategoryCommandInterceptor)
        commandBus.registerDispatchInterceptor(addNewTagToExistingCategoryCommandInterceptor)
    }
}

Thank you @allardbz and @ajisrael for the help.

In the process I learned about the ConfigurerModule. :slight_smile:

1 Like