Your application refuses to start and prints a little box diagram:
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| orderService defined in file [/app/target/classes/com/shop/orders/OrderService.class]
↑ ↓
| invoiceService defined in file [/app/target/classes/com/shop/billing/InvoiceService.class]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.Read the diagram top to bottom and back up: orderService needs invoiceService, which needs orderService. Longer cycles list more beans between the corners. Spring cannot finish creating any bean in the loop, because each one needs the other to exist first.
Quick fix checklist
- Read the box: every bean listed is part of one loop. Find the one dependency edge that should not exist.
- Move the logic both beans need into a new third bean that depends on neither.
- If one side only needs to “notify” the other, publish an application event instead of calling it.
- Check for a bean depending on itself through a
@Configurationclass or a@Beanmethod parameter. - Use
@Lazyon one constructor parameter only as a temporary unblocker, with a ticket to remove it. - Do not set
spring.main.allow-circular-references=trueas the fix; it does not help constructor injection anyway.
Before you start
You should know how Spring creates singleton beans and the difference between constructor, setter and field injection. This article targets Spring Boot 3.x (Spring Framework 6, Java 17). If the classes in the diagram are yours, you can fix the design; if one comes from a library, you will usually solve it in your own configuration.
Why it happens
Spring creates singletons one at a time. To build OrderService with constructor injection, it must first build every constructor argument. When it starts building InvoiceService and finds that it needs OrderService, which is still “currently in creation”, there is no object to hand over yet. With constructor injection there never can be: Java cannot pass an object to a constructor before that object’s own constructor has run.
With field or setter injection Spring has a workaround called early references. It instantiates OrderService with its no-arg constructor, caches a reference to the half-built object, and gives that reference to InvoiceService before OrderService’s fields are filled in. It works, but it lets a bean observe another bean in an incomplete state, and it interacts badly with proxies: if OrderService later gets wrapped in a @Transactional or @Async proxy, InvoiceService may already hold the raw, unproxied instance. Spring detects some of these cases and fails with “has been injected into other beans … in its raw version as part of a circular reference”.
Because of those hazards, Spring Boot 2.6 changed the default: spring.main.allow-circular-references is false, so even field and setter cycles fail at startup. A FailureAnalyzer converts the underlying BeanCurrentlyInCreationException into the box above.
The design reading is more important than the mechanics. Two classes that call each other are usually one responsibility split in the wrong place, or two responsibilities with a missing third collaborator.
Step-by-step walkthrough
Step 1: Reproduce the cycle
@Service
public class OrderService {
private final InvoiceService invoiceService;
public OrderService(InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
public void complete(long orderId) {
// mark order complete...
invoiceService.issueFor(orderId);
}
public BigDecimal totalOf(long orderId) {
return BigDecimal.TEN; // sum of order lines
}
}
@Service
public class InvoiceService {
private final OrderService orderService;
public InvoiceService(OrderService orderService) {
this.orderService = orderService;
}
public void issueFor(long orderId) {
BigDecimal amount = orderService.totalOf(orderId);
// create invoice for amount...
}
}Startup fails with the diagram. Notice what each side actually uses: OrderService calls issueFor, and InvoiceService only needs totalOf, a pricing calculation.
Step 2: Read the failure analysis
The box lists beans in creation order, with the file each came from. For long cycles, write the edges down (A -> B -> C -> A) and annotate each edge with the method that needs it. One edge almost always has a weak reason, such as “calls one helper method”. That is the edge to cut. If the box shows a bean named after a @Configuration class, look for a @Bean method whose parameter is a bean defined in the same class or a class that injects the configuration itself.
Step 3: Extract the shared responsibility
totalOf does not belong to the order workflow; it is pricing. Move it into its own bean that both services can use:
@Service
public class OrderPricing {
private final OrderLineRepository lines;
public OrderPricing(OrderLineRepository lines) {
this.lines = lines;
}
public BigDecimal totalOf(long orderId) {
return lines.findByOrderId(orderId).stream()
.map(OrderLine::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
@Service
public class InvoiceService {
private final OrderPricing pricing;
public InvoiceService(OrderPricing pricing) {
this.pricing = pricing;
}
public void issueFor(long orderId) {
BigDecimal amount = pricing.totalOf(orderId);
// create invoice for amount...
}
}The graph is now a tree: OrderService -> InvoiceService -> OrderPricing. No flag is needed.
Step 4: Or invert the call with an event
When one side only needs to react to something the other did, an event removes the compile-time dependency entirely:
public record OrderCompleted(long orderId) {}
@Service
public class OrderService {
private final ApplicationEventPublisher events;
public OrderService(ApplicationEventPublisher events) {
this.events = events;
}
@Transactional
public void complete(long orderId) {
// mark order complete...
events.publishEvent(new OrderCompleted(orderId));
}
}
@Component
class InvoiceOnOrderCompleted {
private final InvoiceService invoices;
InvoiceOnOrderCompleted(InvoiceService invoices) {
this.invoices = invoices;
}
@TransactionalEventListener
void on(OrderCompleted event) {
invoices.issueFor(event.orderId());
}
}@TransactionalEventListener runs after the order transaction commits by default, so an invoice is never issued for a rolled-back order. Events trade explicit calls for looser coupling, so use them where the relationship really is “something happened”, not for queries that need a return value.
Step 5: Prevent new cycles
Keep constructor injection everywhere so cycles fail at startup, not in production. Add one @SpringBootTest context-load test to CI. For larger codebases, an architecture test (ArchUnit’s slice cycle checks, for example) catches package-level cycles before they become bean cycles.
Worked scenario
A team upgrades from Spring Boot 2.5 to 3.2. The app, which ran for years, now fails with a three-bean cycle:
┌─────┐
| userService (field private com.shop.audit.AuditService com.shop.users.UserService.audit)
↑ ↓
| auditService (field private com.shop.notify.NotificationService com.shop.audit.AuditService.notifications)
↑ ↓
| notificationService (field private com.shop.users.UserService com.shop.notify.NotificationService.users)
└─────┘Every edge uses field injection, which is why Boot 2.5 resolved it silently with early references. The diagnosis comes from annotating each edge: UserService audits user changes, AuditService notifies admins about sensitive changes, and NotificationService calls UserService only to look up an admin’s email address.
That last edge is the weak one. The team replaces it with a dedicated read-only UserDirectory bean backed by the user repository:
@Component
public class UserDirectory {
private final UserRepository users;
public UserDirectory(UserRepository users) {
this.users = users;
}
public Optional<String> emailOf(long userId) {
return users.findById(userId).map(User::getEmail);
}
}NotificationService now depends on UserDirectory, and all three classes switch to constructor injection. The application starts with circular references still prohibited, and the new constructors make each class’s real dependencies visible in code review.
Common mistake
The most tempting wrong fix is pasting the last-resort line from the Action: block:
spring.main.allow-circular-references=trueIt restores the pre-2.6 behaviour for field and setter injection, which means half-initialised beans and the raw-versus-proxy hazard come back. It does nothing for constructor cycles, which remain impossible. Teams that use it to “get the upgrade done” usually never remove it, and new cycles accumulate.
The second tempting fix is switching one side from constructor to field injection. Without the flag it still fails; with the flag it hides the cycle rather than removing it.
@Lazy on a constructor parameter is different and occasionally legitimate:
public InvoiceService(@Lazy OrderService orderService) {
this.orderService = orderService;
}Spring injects a proxy that resolves the real bean on first method call, so construction succeeds. Use it to unblock an urgent upgrade or when a framework callback forces the shape, but treat it as debt: the coupling is still there, the first call pays a lookup, and errors that should appear at startup move to runtime.
Verify the behavior
A context-load test is the regression guard, because the cycle is a startup failure:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ApplicationContextTest {
@Test
void contextLoadsWithoutCircularReferences() {
// fails during context startup if a cycle exists
}
}Make sure no application.properties in src/main or src/test sets spring.main.allow-circular-references=true, otherwise this test proves nothing for field injection. For a focused check, ApplicationContextRunner with only the involved classes registered via withUserConfiguration(...) starts in milliseconds and should report hasNotFailed(). When the app runs, the log shows Started ShopApplication in ... seconds and no BeanCurrentlyInCreationException.
Interview exercise
“Why does Spring Boot reject circular dependencies by default when Spring is technically able to resolve some of them? And when would you accept @Lazy as a fix?”
Answer and reasoning
Spring can resolve setter and field cycles by exposing an early reference to a bean that is not fully initialised. That reference may skip later post-processing such as proxy creation, so another bean could hold an object without transactional or async behaviour, and any code that uses the dependency during initialisation sees a partially built object. Constructor cycles cannot be resolved at all. Boot 2.6 made the safe choice the default and surfaces cycles as a design signal. The right response is to find the weakest edge and move that responsibility into a separate bean or replace the call with an event. I would accept @Lazy only as a time-boxed measure, for example during a framework upgrade or when a third-party bean forces the cycle, with a test that keeps the context loading and a follow-up to remove it, because it defers failures from startup to the first call.