Skip to content

Fix: Spring Boot "The dependencies of some of the beans in the application context form a cycle"

FixDevs · (Updated: )

Part of:  Java & JVM Errors

Quick Answer

How to fix Spring Boot circular dependency errors — BeanCurrentlyInCreationException, refactoring to break cycles, @Lazy injection, setter injection, and @PostConstruct patterns.

The Error

Spring Boot fails to start with:

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

Description:

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

userService defined in file [.../UserService.class]

emailService defined in file [.../EmailService.class]

userService (field com.example.UserService com.example.EmailService.userService)

Or in older Spring versions:

org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'userService':
Requested bean is currently in creation:
Is there an unresolvable circular reference?

Or after upgrading to Spring Boot 2.6+:

Spring Boot 2.6 changed the default setting for spring.main.allow-circular-references to false.
If you need to allow for circular references, set this property to true.

Why This Happens

A circular dependency exists when Bean A requires Bean B to be created, and Bean B requires Bean A to be created — forming a loop that Spring cannot resolve through constructor injection.

Spring Boot 2.6+ disabled circular dependencies by default because they often indicate a design problem:

  • Service layer cyclesUserService depends on EmailService, and EmailService depends on UserService.
  • Controller depending on itself — a controller autowires a service that autowires the same controller.
  • Shared utility bean — two services both depend on each other through a shared operation that should be extracted into its own component.
  • Constructor injection reveals the problem — with constructor injection, circular dependencies always fail at startup. With field injection, they may work via proxy magic but still represent a design flaw.

Why this matters: Circular dependencies signal that responsibilities aren’t cleanly separated. A dependency cycle means two classes know too much about each other. Fixing the design produces more maintainable, testable code.

The error surface is wider than just two services pointing at each other. A @ConfigurationProperties class that pulls in a service that needs the same config, an @Aspect that wraps a bean it depends on, or a Spring Cloud BootstrapConfiguration that loads before the main context — all of them can introduce cycles you never wrote on purpose. Spring’s autowiring resolves these in declaration order, so adding one new field can flip a startup that worked yesterday into a cycle failure today.

A second pattern that catches teams off guard is the constructor versus field injection asymmetry. Old codebases used @Autowired field injection everywhere, which let Spring create the empty bean first and fill the fields later. When that codebase modernizes to constructor injection (recommended since Spring 4.3), latent cycles surface immediately at startup. The cycle was always there — Spring was just hiding it behind reflective field assignment. Migrating away from field injection is good for testability and immutability, but expect to fix a wave of “form a cycle” errors during the migration.

Version History That Changes the Failure Mode

The behavior around circular references has shifted across Spring Boot versions. Knowing which version you’re on determines whether the symptom is a startup failure, a warning, or silent proxy injection.

Spring 4.3 (June 2016) — implicit constructor injection. Before 4.3, a single-constructor @Component still required @Autowired on the constructor. After 4.3, Spring infers the autowiring. This is the version that quietly migrated communities toward constructor injection, which in turn started exposing cycles that field injection used to hide.

Spring Framework 5.0 / Spring Boot 2.0 (2017–2018). Cycles were still allowed by default. @Lazy worked, setter injection worked, and the framework would log a warning to a hidden category but continue. Code from this era often has dormant cycles wrapped in @Lazy “for performance” — the comment is wrong, the real reason was to silence a cycle.

Spring Boot 2.6 (November 2021) — spring.main.allow-circular-references defaults to false. This is the change that broke half of all upgrades from 2.5 to 2.6. Applications that had run for years suddenly failed at startup with the multi-bean cycle diagram in the logs. The fast workaround was to flip the flag back to true, which AWS, GCP, and most internal frameworks ended up doing en masse. The slow fix was to actually break the cycle — see Fix 1.

Spring Boot 2.7 (May 2022). No behavior change on cycles, but allow-circular-references was officially marked as a temporary escape hatch and the recommendation hardened: fix the design.

Spring Boot 3.0 (November 2022). Baseline jumped to Java 17 and Jakarta EE 9 (jakarta.* packages instead of javax.*). The cycle default stayed at false. The migration broke any AspectJ or proxy library still on javax.inject — and because those libraries often inserted themselves into the bean graph, the failure mode for cycles on 3.0 now sometimes manifests as ClassNotFoundException for a javax.* import before the cycle is even detected. Run mvn dependency:tree | grep javax.inject before trusting the cycle diagnostic on 3.0.

Spring Boot 3.1 (May 2023). Introduced improved diagnostics for bean introspection. The cycle report now includes the source file path for each bean, not just the class name. This is purely a UX improvement, but it means cycle errors copy-pasted from a 3.1+ log into a search engine have richer context than older errors.

Spring Boot 3.2 / 3.3 (2023–2024). Native image support via GraalVM matured, and the AOT processor now flags unresolvable cycles at build time instead of at startup. If you run ./mvnw spring-boot:process-aot, you’ll see the cycle in a META-INF/native-image/ report before the JVM is even invoked.

Deprecated flag track. spring.main.allow-circular-references is still functional on 3.x but the Spring team has signaled it will eventually be removed. Code that depends on it is now formally tech debt — treat it as a temporary unblock, not a permanent setting.

Fix 1: Refactor to Break the Cycle (Best Fix)

The correct fix is to restructure your code so the cycle doesn’t exist. The most common approach: extract the shared logic into a third component.

Before — circular dependency:

@Service
public class UserService {
    private final EmailService emailService;  // UserService → EmailService

    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }

    public User createUser(String email) {
        User user = userRepository.save(new User(email));
        emailService.sendWelcomeEmail(user);  // UserService calls EmailService
        return user;
    }
}

@Service
public class EmailService {
    private final UserService userService;  // EmailService → UserService (CYCLE!)

    public EmailService(UserService userService) {
        this.userService = userService;
    }

    public void sendWelcomeEmail(User user) {
        String name = userService.getDisplayName(user);  // EmailService calls UserService
        // send email with name
    }
}

After — extract the shared logic:

@Service
public class UserService {
    private final EmailService emailService;

    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }

    public User createUser(String email) {
        User user = userRepository.save(new User(email));
        emailService.sendWelcomeEmail(user.getEmail(), user.getDisplayName());
        return user;
    }

    public String getDisplayName(User user) {
        return user.getFirstName() + " " + user.getLastName();
    }
}

@Service
public class EmailService {
    // EmailService no longer depends on UserService
    private final JavaMailSender mailSender;

    public EmailService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    // Accept the data it needs as parameters — no need to call UserService
    public void sendWelcomeEmail(String email, String displayName) {
        // send email
    }
}

Another common pattern — extract to an event or mediator:

// Instead of direct service-to-service calls, use Spring events
@Service
public class UserService {
    private final ApplicationEventPublisher eventPublisher;

    public UserService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public User createUser(String email) {
        User user = userRepository.save(new User(email));
        eventPublisher.publishEvent(new UserCreatedEvent(user));  // Decouple via event
        return user;
    }
}

@Service
public class EmailService {
    // Listens for UserCreated events — no dependency on UserService
    @EventListener
    public void handleUserCreated(UserCreatedEvent event) {
        User user = event.getUser();
        sendWelcomeEmail(user.getEmail(), user.getDisplayName());
    }
}

Fix 2: Use @Lazy to Defer Injection

@Lazy tells Spring to inject a proxy instead of the real bean at construction time. The real bean is created only when a method is first called. This breaks the construction-time cycle without redesigning the classes:

@Service
public class UserService {
    private final EmailService emailService;

    // @Lazy on one of the two injections breaks the cycle
    public UserService(@Lazy EmailService emailService) {
        this.emailService = emailService;
    }
}

@Service
public class EmailService {
    private final UserService userService;

    public EmailService(UserService userService) {
        this.userService = userService;
    }
}

Or use @Lazy on the field (with field injection):

@Service
public class UserService {

    @Lazy
    @Autowired
    private EmailService emailService;
}

Note: @Lazy is a workaround, not a design fix. Spring injects a CGLIB proxy at construction time and calls the real bean when the method is invoked. This adds a small runtime overhead and can cause confusing behavior in tests. Prefer Fix 1 (refactoring) when possible.

Fix 3: Switch from Constructor Injection to Setter Injection

Spring can resolve circular dependencies through setter injection (but not constructor injection) because with setter injection, the beans are first created (empty) and then wired together:

@Service
public class UserService {
    private EmailService emailService;

    // Setter injection — Spring creates UserService first, then injects EmailService
    @Autowired
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
}

@Service
public class EmailService {
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

Warning: Setter injection makes dependencies optional by default — Spring won’t fail at startup if a bean is missing (it will fail only when the method is called). Use @Autowired(required = true) if the dependency is mandatory. Like @Lazy, setter injection is a workaround. Prefer refactoring.

Fix 4: Use @PostConstruct for Initialization Logic

Sometimes the cycle occurs because a bean calls another bean during its own construction. Move the initialization logic to @PostConstruct, which runs after all beans are wired:

@Service
public class CacheService {
    private final UserService userService;
    private List<User> cachedUsers;

    public CacheService(UserService userService) {
        this.userService = userService;
        // Don't call userService here — it may not be ready yet
    }

    @PostConstruct
    public void init() {
        // Called after all beans are wired — safe to call userService
        this.cachedUsers = userService.findAll();
    }
}

Fix 5: Allow Circular References (Last Resort)

Spring Boot 2.6+ blocks circular references by default. You can re-enable them as a temporary workaround while you fix the underlying design:

# application.yml — allow circular references (temporary fix only)
spring:
  main:
    allow-circular-references: true

Or in application.properties:

spring.main.allow-circular-references=true

Warning: Setting allow-circular-references=true re-enables the old behavior that Spring Boot deliberately removed. It masks the design problem and makes your application harder to reason about. Use it only as a short-term fix to keep the application running while you implement a proper structural fix. Track it as technical debt.

Fix 6: Diagnose the Full Dependency Cycle

When the cycle involves multiple beans, the error message shows the full chain. Read it carefully:

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

orderService → paymentService → notificationService → userService → orderService

This tells you the cycle is: orderService → paymentService → notificationService → userService → orderService.

Use Spring’s dependency graph to investigate:

// Add to your application — temporarily log all bean dependencies
@Configuration
public class BeanInspector implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        ConfigurableApplicationContext context = (ConfigurableApplicationContext) ctx;
        BeanFactory factory = context.getBeanFactory();

        if (factory instanceof DefaultListableBeanFactory) {
            DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) factory;
            for (String name : beanFactory.getBeanDefinitionNames()) {
                BeanDefinition def = beanFactory.getBeanDefinition(name);
                String[] deps = def.getDependsOn();
                if (deps != null) {
                    System.out.println(name + " depends on: " + Arrays.toString(deps));
                }
            }
        }
    }
}

Enable debug logging to see Spring’s bean creation order:

logging:
  level:
    org.springframework.beans.factory: DEBUG

Still Not Working?

Check if the cycle is in test configuration. Spring test contexts can introduce cycles that don’t exist in production if test configuration beans add extra dependencies:

@TestConfiguration
public class TestConfig {
    // Beans defined here can create cycles with production beans
}

Check if @Component, @Service, or @Repository is accidentally applied to a class that should not be a Spring bean. An unexpected component scan can pull in a class that creates a cycle.

Check for @Configuration class cycles. Circular dependencies between @Configuration classes (where one @Bean method calls another @Configuration class’s @Bean method) also cause this error:

@Configuration
public class ConfigA {
    @Bean
    public BeanA beanA(BeanB beanB) { return new BeanA(beanB); }  // Requires BeanB
}

@Configuration
public class ConfigB {
    @Bean
    public BeanB beanB(BeanA beanA) { return new BeanB(beanA); }  // Requires BeanA → CYCLE
}

Fix: inject by method reference instead of by parameter, or merge related configuration into a single @Configuration class.

Check for AOP proxy interference. @Aspect classes that advise a bean and also depend on it create cycles that the diagnostic message attributes to the advised bean, not the aspect. Run with logging.level.org.springframework.aop=DEBUG and look for ProxyFactory activity around the cycle nodes. The fix is usually to move the aspect’s dependency behind an ObjectProvider<T> so Spring resolves it lazily.

Check whether AOT processing flags the cycle at build time. On Spring Boot 3.2+, run ./mvnw spring-boot:process-aot (or the Gradle equivalent) and inspect target/spring-aot/. AOT processing surfaces cycles before the JVM is even started, which is faster than a 30-second startup-fail loop. If you’re targeting GraalVM native images, AOT cycle detection is mandatory — the native binary will not start otherwise.

Check for jakarta.* vs javax.* mismatches on Spring Boot 3.x. A dependency still pulling javax.inject or javax.annotation can manifest as a confusing cycle diagnostic because Spring fails to resolve the annotated bean entirely. Run ./mvnw dependency:tree and grep for javax.inject and javax.annotation. Any match needs an exclusion or a version bump.

For related Spring Boot issues, see Fix: Spring Boot DataSource Failed, Fix: Spring Security Returning 403 Forbidden, Fix: Spring Boot BeanCreationException, and Fix: NestJS Circular Dependency.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles