Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring circular dependencies: break the constructor graph

Last updated: 1 Oct 20265 min read
tutorial
IntermediateBy AITrove Editorial

A constructor dependency cycle exists when creating bean A requires bean B while creating B requires A. Neither constructor can finish first, so the container rejects the graph.

Find the actual ownership mistake

A receipt service sometimes calls an audit service, while the audit service calls back into the receipt service to fetch display fields. That is a design cycle before it is a Spring error. Passing the required fields to the audit operation removes the backward lookup and makes the data ownership visible.

Java
package in.aitrove.receipts;

import org.springframework.stereotype.Service;

record AuditRecord(long receiptId, String action) {}

interface AuditSink {
    void append(AuditRecord record);
}

@Service
class ReceiptWorkflow {
    private final ReceiptStore receiptStore;
    private final AuditSink auditSink;

    ReceiptWorkflow(ReceiptStore receiptStore, AuditSink auditSink) {
        this.receiptStore = receiptStore;
        this.auditSink = auditSink;
    }

    void approve(long receiptId) {
        receiptStore.approve(receiptId);
        auditSink.append(new AuditRecord(receiptId, "APPROVED"));
    }
}

interface ReceiptStore {
    void approve(long receiptId);
}

The two writes above are not automatically atomic. If audit is a required database record, use a transaction that covers both rows and test rollback. If audit goes to an external service, a local transaction cannot commit that remote call with the receipt; use a durable delivery design where the requirement demands it.

Do not conceal the cycle

Changing one side to field injection, @Lazy, or a provider may delay resolution, but it does not answer why two services need to own each other. Deferred lookup has legitimate uses when creation really must be late; it should not be the default repair for an accidental cycle.

For n beans and m dependency edges, a graph walk to identify a cycle is O(n + m). Runtime invocation cost is a separate question: replacing an in-process call with a remote audit call changes latency and failure modes far more than replacing constructor injection with a provider.

Common Mistakes

  • Assuming a successful context startup proves two writes are atomic.
  • Using a lazy proxy to postpone a design error until the first request.
  • Making the audit adapter query the workflow for data it could receive as an argument.

Read next

ApplicationContext ownership, transaction proxy calls, and the transactional outbox.

spring
spring-boot
circular-constructor-graph
Storage details