What changed in 2.6, and what did not
Spring Framework itself has long been able to satisfy some circular references. Spring Boot 2.6 changed the default it applies to the application context, so a cycle is now a startup failure unless you opt back in. The property above is the opt-in, and SpringApplication.setAllowCircularReferences(true) does the same in code. Boot's failure analyser prints the cycle as a ring of bean names, which is the most useful output in the whole log. Read it before touching any configuration.
What did not change is that a cycle through constructor injection never worked. If OrderService needs a CustomerService in its constructor and CustomerService needs an OrderService in its own, neither can be built first. Spring throws BeanCurrentlyInCreationException with or without the property. So an app that started cleanly before the upgrade had its cycle running through field or setter injection, and the upgrade did not create the problem. It stopped tolerating one that was already there.
How the cycle used to start
With field injection, Spring can construct OrderService with no dependencies, register an early reference to it, and then go to build CustomerService. When CustomerService asks for an OrderService, it receives that early reference: an object whose constructor has run but whose fields are not yet populated. Spring then finishes CustomerService, injects it into OrderService, and both beans end up wired.
The danger is the window in the middle. If CustomerService calls into OrderService from a @PostConstruct method, it is calling an object whose own dependencies may still be null. The result is a NullPointerException at startup that depends on bean creation order, which can change when someone adds an unrelated bean.
Proxies make it worse. The early reference is handed out before the post-processors that create AOP proxies have run on OrderService. For @Transactional, Spring can create the proxy early and hand that out instead. Some post-processors cannot, with @Async the classic example. There Spring notices that another bean received the raw object before it was wrapped, and fails startup. The alternative would be a bean holding an unproxied reference whose annotations silently do nothing. Prohibiting cycles by default removes that whole class of order-dependent behaviour.
Turning the check off, and when that is reasonable
Flipping the property is a legitimate move in one situation: you are mid-upgrade, the cycle predates you, and fixing it properly would block a release that carries other changes. Set it, add a comment naming the cycle and a ticket, and treat the property as a debt with an owner. What you should not do is set it permanently. You would be re-enabling the half-built-bean window above, and the next cycle someone writes will start silently too.
The other quick fix deserves the same suspicion. Putting @Lazy on one constructor parameter makes Spring inject a lazy-resolution proxy, which resolves the real bean on first use. Startup passes, and the cycle is still there, now behind a proxy that hides it from the failure analyser and from anyone reading the constructors. @Lazy has honest uses. A habit of using it to quiet cycle errors is how a codebase accumulates them.
Breaking the cycle
A cycle between two services usually means each holds something the other needs, and that something wants to be its own bean. In the typical case, OrderService asks CustomerService for a credit limit when placing an order. Meanwhile CustomerService asks OrderService for order totals to compute lifetime value. The shared responsibility is reading order totals, and neither service should own it alone.
The dependency graph is now a line, and all three classes use constructor injection, so reintroducing the cycle fails at startup on the developer's machine.
Extraction is the usual answer, not the only one. When one direction is a notification rather than a query, such as an order telling the customer side that something happened, an application event inverts it. The publisher no longer knows who listens. A plain @EventListener runs synchronously in the publisher's thread and transaction, while @TransactionalEventListener defaults to running after commit. Choose deliberately, because that choice decides whether a listener failure rolls back the order. Sometimes the cycle shows that the two classes are one responsibility split badly, and merging them is honest. Passing the needed value as a method argument, instead of injecting a whole service to fetch it, removes many small cycles outright.
What separates a strong answer
Most candidates know the property exists. The answer that scores names the 2.6 default change precisely, and explains why the old behaviour was risky rather than merely deprecated. It refuses @Lazy as a reflex and arrives at a refactor that names the missing third bean. Proposing constructor injection across the codebase as the guard, so the container rejects the next cycle on its own, shows you are preventing a class of problem rather than closing one ticket.
The property buys you time; the cycle is still a design defect. Extract what both sides need, keep injection in constructors, and let the container reject the next cycle for you.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).