Skip to content

Custom @Bean EventStorageEngine silently disables @RegisterDefaultEntities #4574

@MateuszNaKodach

Description

@MateuszNaKodach

Basic information

Steps to reproduce

  1. Spring Boot 3.5 application on axoniq-spring-boot-starter:5.1.1-SNAPSHOT with Hibernate JPA + Postgres (or any JPA provider), axon.axonserver.enabled: false, ddl-auto: update.
  2. Declare an explicit @Bean EventStorageEngine to deterministically select AggregateBasedJpaEventStorageEngine over the axon-server-connector's ServiceLoader-registered default (this is necessary whenever the connector is on the classpath — see "Why the user bean is necessary" below):
    @Configuration
    public class EventStoreConfiguration {
    
        @Bean
        public EventStorageEngine eventStorageEngine(EntityManagerFactory emf,
                                                      EventConverter eventConverter) {
            return new AggregateBasedJpaEventStorageEngine(
                    new JpaTransactionalExecutorProvider(emf),
                    eventConverter,
                    UnaryOperator.identity()
            );
        }
    }
  3. Do not add any @EntityScan to the @SpringBootApplication class. Rely on AF5's @RegisterDefaultEntities mechanism to register AggregateEventEntry.
  4. Run a @SpringBootTest that exercises the JPA event store (any test that boots the full context against a fresh Postgres — Testcontainers will do).

The application context fails to load. Stack trace shows:

org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity 'AggregateEventEntry'

Expected behaviour

AggregateEventEntry should be registered with the application's EntityManagerFactory via the AF5 @RegisterDefaultEntities(packages = {"org.axonframework.eventsourcing.eventstore.jpa"}) declaration on JpaEventStoreAutoConfiguration — the mechanism explicitly documented to make user-side @EntityScan for framework JPA entities unnecessary. The user supplying their own EventStorageEngine should not affect entity discovery: the engine bean and the persistence-metamodel registration are orthogonal concerns.

Actual behaviour

JpaEventStoreAutoConfiguration carries both of these annotations:

@ConditionalOnMissingBean(value = {EventStore.class, EventStorageEngine.class})
@RegisterDefaultEntities(packages = {
        "org.axonframework.eventsourcing.eventstore.jpa"
})
public class JpaEventStoreAutoConfiguration { ... }

(extensions/spring/spring-boot-autoconfigure/src/main/java/org/axonframework/extension/springboot/autoconfig/JpaEventStoreAutoConfiguration.java, lines 61–65.)

Spring's ConfigurationClassParser evaluates class-level @Conditional annotations before processing @Import. As soon as the user defines a @Bean EventStorageEngine, the class-level @ConditionalOnMissingBean(EventStorageEngine.class) evaluates to false and the entire @AutoConfiguration class is filtered out — including its @Import(DefaultEntityRegistrar.class) (declared by @RegisterDefaultEntities, see …/util/RegisterDefaultEntities.java). The DefaultEntityRegistrar.registerBeanDefinitions(...) callback never runs, so AutoConfigurationPackages.register(registry, "org.axonframework.eventsourcing.eventstore.jpa") is never called, so Spring Boot's JpaBaseConfiguration.getPackagesToScan() never sees the framework package, so Hibernate's metamodel never picks up AggregateEventEntry.

The bug cascades as soon as the user reaches for the obvious workaround. Adding @EntityScan(basePackages = "org.axonframework.eventsourcing.eventstore.jpa") recovers AggregateEventEntry, but the next test run fails with:

org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity 'TokenEntry'

JpaAutoConfiguration (.../JpaAutoConfiguration.java, lines 55–57) was registering TokenEntry via its own @RegisterDefaultEntities. That registration is silently dropped because Spring Boot's JpaBaseConfiguration.getPackagesToScan() prefers EntityScanPackages (from @EntityScan) over AutoConfigurationPackages when an @EntityScan is declared. The user must then keep widening @EntityScan until it covers every framework JPA package that ships an entity. AxoniqFramework's JpaDeadLetterQueueAutoConfiguration for io.axoniq.framework.messaging.eventhandling.deadletter.jpa (DeadLetterEntry) is dropped by the same displacement.

End state observed in the reproducer: the user's @SpringBootApplication carries

@EntityScan(basePackages = {
        "com.dddheroes.heroesofddd",
        "org.axonframework",
        "io.axoniq.framework"
})

to recover green tests. This is the minimum that covers AggregateEventEntry, TokenEntry, and DeadLetterEntry, and must be widened again whenever a new framework module ships a JPA entity. The @RegisterDefaultEntities mechanism is, in this configuration, entirely non-functional.

Failure trail (./mvnw test -Dtest='GetDwellingByIdTest' at commit 77d5ea3)

  1. org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [com.dddheroes.heroesofddd.utils.AggregateEventPublisher ...] — JUnit error masking the real failure (constructor autowiring disabled because @TestConstructor default mode is ANNOTATED). Fix via src/test/resources/spring.properties with spring.test.constructor.autowire.mode=all.
  2. org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity 'AggregateEventEntry' — the root issue described here.
  3. After @EntityScan(basePackages = "org.axonframework.eventsourcing.eventstore.jpa"): Could not resolve root entity 'TokenEntry'.
  4. After expanding @EntityScan to cover org.axonframework and io.axoniq.framework: green.

Why the user bean is necessary

Without an explicit @Bean EventStorageEngine, axon-server-connector's ServiceLoader-discovered AxonServerConfigurationEnhancer (order Integer.MIN_VALUE + 10) registers AxonServerEventStorageEngine via registerIfNotPresent(..., SearchScope.ALL) before JpaEventStoreAutoConfiguration's enhancer (order ≈ Integer.MAX_VALUE - 600) gets a chance — even with axon.axonserver.enabled=false, which only gates the Spring @Bean methods on AxonServerAutoConfiguration, not the ServiceLoader-discovered enhancer. The user-provided Spring @Bean is the only deterministic way to make JPA win, via SpringComponentRegistry.hasComponent(..., SearchScope.ALL) consulting the Spring BeanFactory before either enhancer runs. So the entity-registration coupling bites exactly the users who need the explicit @Bean most.

Notes

I'm not sure this is cleanly fixable on the framework side — moving @RegisterDefaultEntities off the engine-gated autoconfig would register AggregateEventEntry for users running pure Axon Server (where the entity table is schema pollution they don't want). The current coupling may be intentional, gated by @ConditionalOnMissingBean to express "register entities only when this autoconfig is going to provide the engine".

If it can't be fixed, the minimum-viable outcome is to document this in the AF4 → AF5 migration paths for the event-storage-engine: whenever the migration recipe instructs the user to declare an explicit @Bean EventStorageEngine (Path A on axoniq-spring-boot-starter, or any Path B variant), pair that instruction with a note that @EntityScan(basePackages = {<app-package>, "org.axonframework", "io.axoniq.framework"}) is also required, and warn that a partial @EntityScan silently disables @RegisterDefaultEntities contributions from sibling autoconfigs. That doc-only outcome is acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority 2: ShouldHigh priority. Ideally, these issues are part of the release they’re assigned to.

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions