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

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
ca-skeleton
error
spring-boot
circular-dependency
flyway
jpa
2026-06-23 2026-06-23

Spring JPA-Flyway Circular Dependency during Context Initialization

Parent

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

  1. The configuration class PostgreSqlPersistenceConfig uses @PersistenceContext to inject the JPA EntityManager:
    @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:
    @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:

@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 @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:
    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)