5.6 KiB
title, source_type, status, branch, related_projects, tags, created, updated
| title | source_type | status | branch | related_projects | tags | created | updated | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| error / spring-jpa-flyway-circular-dependency-2026-06-23 | error-note | raw | feature-build-release-supply-chain-contract |
|
|
2026-06-23 | 2026-06-23 |
Spring JPA-Flyway Circular Dependency during Context Initialization
Parent
- Parent branch note: raw/branch-notes/feature-build-release-supply-chain-contract
Symptoms
During application startup using bootRun, the application context failed to initialize with a circular dependency error:
Description:
The dependencies of some of the beans in the application context form a cycle:
entityManagerFactory defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]
┌─────┐
| flyway defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]
↑ ↓
| postgreSqlPersistenceConfig
└─────┘
When running without a local database connection, this circularity prevented the application from failing fast with a connection error and instead produced secondary errors such as NoSuchBeanDefinitionException during context shutdown (e.g. No bean named 'org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry' available).
Root Cause
- The configuration class
PostgreSqlPersistenceConfiguses@PersistenceContextto inject the JPAEntityManager:@PersistenceContext private EntityManager entityManager; - Creating
PostgreSqlPersistenceConfigtherefore requires theEntityManager(and consequentlyEntityManagerFactory) to be initialized and available. - In
PostgreSqlPersistenceConfig, a customizer bean was declared as a non-static@Bean:@Bean public FlywayConfigurationCustomizer postgreSqlFlywayLocationCustomizer() { return configuration -> configuration.locations("classpath:db/migration/postgresql"); } - Because it is a non-static
@Beanmethod, Spring requires instantiatingPostgreSqlPersistenceConfigbefore it can call the method to register the customizer. - However:
EntityManagerFactorydepends onflywayInitializer(to ensure migrations run first).flywayInitializerdepends on theFlywaybean.- The
Flywaybean depends on all registeredFlywayConfigurationCustomizerbeans. - Spring tries to resolve
FlywayConfigurationCustomizer-> instantiatesPostgreSqlPersistenceConfig-> injectsEntityManager-> createsEntityManagerFactory-> waits forflywayInitializer-> waits forFlyway-> waits forFlywayConfigurationCustomizer. - This forms a cycle:
EntityManagerFactory->flywayInitializer->Flyway->PostgreSqlPersistenceConfig->EntityManagerFactory.
Solution
1. Make the Flyway customizer static
Change the FlywayConfigurationCustomizer bean declaration to a static @Bean method in both PostgreSqlPersistenceConfig.java and SamplePostgreSqlPersistenceConfig.java:
@Bean
public static FlywayConfigurationCustomizer postgreSqlFlywayLocationCustomizer() {
return configuration -> configuration.locations("classpath:db/migration/postgresql");
}
This allows Spring to invoke the customizer registration without instantiating the enclosing configuration class, breaking the immediate EntityManagerFactory dependency cycle.
2. Refactor @PersistenceContext Field Injection to Parameter Injection
To prevent the configuration classes from triggering early instantiation of the JPA infrastructure (which causes NoSuchBeanDefinitionException: No bean named 'org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry' available), eliminate the class-level EntityManager field injection and instead inject EntityManager as a parameter to the factory @Bean methods:
Before:
@Configuration
public class PostgreSqlPersistenceConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public OutboxClaimRepository outboxClaimRepository() {
return new PostgreSqlOutboxClaimRepository(entityManager);
}
}
After:
@Configuration
public class PostgreSqlPersistenceConfig {
@Bean
public OutboxClaimRepository outboxClaimRepository(EntityManager entityManager) {
return new PostgreSqlOutboxClaimRepository(entityManager);
}
}
Why it works
- Static customizer: Decouples customizer registration from the instantiation of the enclosing
@Configurationclass. - Parameter injection: Prevents Spring's configuration class post-processor from resolving the
EntityManagerbean prematurely during class creation, postponing its resolution until the specific factory method is executed. This completely avoids the early bootstrap lifecycle cycle.
Verification & Outcomes
Local Verification
- Ensured the local database container
ca-pgis running on port5432. - Cleaned and recreated the database using
psqlto clear any checksum mismatch issues. - Ran
./gradlew :app-bootstrap:bootRun. - The application initialized the connection pool, ran Flyway migrations, and successfully booted:
2026-06-23 14:32:00.619 INFO [main] o.f.core.internal.command.DbMigrate - Successfully applied 3 migrations to schema "public", now at version v4 (execution time 00:00.085s) ... 2026-06-23 14:32:03.278 INFO [main] d.c.bootstrap.CaSkeletonApplication - Started CaSkeletonApplication in 4.87 seconds (process running for 5.034)