Files
llm-wiki/raw/errors/spring-jpa-flyway-circular-dependency-2026-06-23.md

117 lines
5.6 KiB
Markdown

---
title: error / spring-jpa-flyway-circular-dependency-2026-06-23
source_type: error-note
status: raw
branch: feature-build-release-supply-chain-contract
related_projects: [ca-skeleton]
tags: [error, spring-boot, circular-dependency, flyway, jpa]
created: 2026-06-23
updated: 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:
```text
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
1. The configuration class `PostgreSqlPersistenceConfig` uses `@PersistenceContext` to inject the JPA `EntityManager`:
```java
@PersistenceContext
private EntityManager entityManager;
```
2. Creating `PostgreSqlPersistenceConfig` therefore requires the `EntityManager` (and consequently `EntityManagerFactory`) to be initialized and available.
3. In `PostgreSqlPersistenceConfig`, a customizer bean was declared as a non-static `@Bean`:
```java
@Bean
public FlywayConfigurationCustomizer postgreSqlFlywayLocationCustomizer() {
return configuration -> configuration.locations("classpath:db/migration/postgresql");
}
```
4. Because it is a non-static `@Bean` method, Spring requires instantiating `PostgreSqlPersistenceConfig` *before* it can call the method to register the customizer.
5. However:
- `EntityManagerFactory` depends on `flywayInitializer` (to ensure migrations run first).
- `flywayInitializer` depends on the `Flyway` bean.
- The `Flyway` bean depends on all registered `FlywayConfigurationCustomizer` beans.
- Spring tries to resolve `FlywayConfigurationCustomizer` -> instantiates `PostgreSqlPersistenceConfig` -> injects `EntityManager` -> creates `EntityManagerFactory` -> waits for `flywayInitializer` -> waits for `Flyway` -> waits for `FlywayConfigurationCustomizer`.
- 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`:
```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:**
```java
@Configuration
public class PostgreSqlPersistenceConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public OutboxClaimRepository outboxClaimRepository() {
return new PostgreSqlOutboxClaimRepository(entityManager);
}
}
```
**After:**
```java
@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 `@Configuration` class.
- **Parameter injection**: Prevents Spring's configuration class post-processor from resolving the `EntityManager` bean 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
1. Ensured the local database container `ca-pg` is running on port `5432`.
2. Cleaned and recreated the database using `psql` to clear any checksum mismatch issues.
3. Ran `./gradlew :app-bootstrap:bootRun`.
4. The application initialized the connection pool, ran Flyway migrations, and successfully booted:
```text
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)
```