Dependency Injection in Java — Circular Refs That Crash
BeanCurrentlyInCreationException kills startup when two beans inject each other via constructor.
- Dependency Injection (DI) is a technique where an external container provides a class its dependencies rather than the class creating them itself
- Three injection styles: constructor (most preferred), setter (optional deps), field (fragile, test-unfriendly)
- IoC container scans class dependencies, builds a graph, and wires them at startup
- Spring uses reflection to inject beans: constructor injection avoids reflection overhead for required deps
- Biggest mistake: circular dependencies — they compile but throw BeanCurrentlyInCreationException at runtime
- Performance insight: field injection uses reflection every time — for high-throughput beans, prefer constructor injection
Imagine you run a coffee shop. Instead of your barista going out to buy milk every morning, a supplier just delivers it to the door. The barista doesn't care where the milk came from — they just use it. Dependency Injection works the same way: instead of your class hunting down its own dependencies (like database connections or services), something else just hands them over. Your class stays focused on its actual job, and swapping the 'milk supplier' later requires zero changes to the barista.
Every Java application beyond 'Hello World' has objects that depend on other objects. A UserService needs a UserRepository. A PaymentProcessor needs a NotificationClient. The way you wire those relationships together determines how testable, maintainable, and scalable your codebase will be — arguably more than any other single design decision. Get it wrong and you end up with a tightly-coupled monolith where changing one class breaks five others and mocking anything in a unit test requires heroic effort.
Dependency Injection (DI) solves the coupling problem by inverting control: instead of a class creating or locating its own dependencies, an external mechanism provides them. This is the practical application of the Dependency Inversion Principle (the D in SOLID). The result is code where each class declares what it needs without caring how those needs are fulfilled — making it trivially easy to swap implementations, inject mocks in tests, and reason about each class in isolation.
By the end of this article you'll understand the three injection styles and when each one is appropriate, how an IoC container actually resolves a dependency graph at runtime, how Spring implements DI under the hood, and the real-world gotchas that trip up experienced engineers — circular dependencies, prototype beans inside singletons, and the performance cost of reflection-based injection. You'll leave with patterns you can apply tomorrow morning.
What Is Dependency Injection?
Dependency Injection (DI) is a technique where an object receives other objects it depends on from an external source, rather than creating them itself. This external source is called an Inversion of Control (IoC) container. The term 'inversion' refers to the shifted responsibility: your class no longer controls dependency creation — the container does.
In Java, DI is implemented via three primary injection styles: constructor injection (dependencies passed via the constructor), setter injection (dependencies set via setter methods), and field injection (dependencies injected directly onto private fields via reflection).
Constructor injection is the most reliable — it ensures the object is fully initialized upon creation and enables immutability. Field injection, while convenient, introduces testability problems because you can't easily provide mocks without the container. Setter injection sits in the middle: useful for optional dependencies but requires the object to tolerate a partially constructed state.
- Your class declares what it needs (constructor parameters)
- The container decides when and how to create those ingredients (beans)
- If the supplier changes (different implementation), the chef never notices
- Testing = substitute the real ingredients with fake ones (mocks)
How an IoC Container Resolves the Dependency Graph
When you annotate a class with @Component, @Service, @Repository, or @Controller, Spring's IoC container picks it up during component scanning. It then builds a dependency graph by inspecting each bean's constructors, fields, and setters (depending on the injection strategy).
The container uses a process called 'bean post-processing' to determine the order of instantiation. It first creates beans with no dependencies, then progressively creates those that depend on already-created beans. This is essentially a topological sort of the dependency graph.
If a circular dependency is detected — bean A needs bean B which needs bean A — Spring will throw a BeanCurrentlyInCreationException at startup, unless one of the dependencies uses @Lazy (which breaks the cycle by deferring the creation of the lazy bean until it's actually accessed). Under the hood, Spring uses a 'singleton currently in creation' set to track beans during construction.
AbstractAutowireCapableBeanFactory.populateBean() is where the magic happens. It processes @Autowired fields, @Inject annotations, and setter methods. Constructor resolution happens earlier in AutowireUtils.resolveAutowiring() using the same topological logic.Spring's Autowiring and Qualifiers
Spring's autowiring resolves dependencies by type first, then by qualifier if multiple beans of the same type exist. When a constructor parameter type matches exactly one bean, Spring injects it. If there are multiple beans, Spring tries to match the parameter name to the bean name — and if the name doesn't match, you must use @Qualifier.
This is a common source of head-scratching bugs: you add a new implementation of an interface, and suddenly startup fails with 'NoUniqueBeanDefinitionException' because Spring doesn't know which one to use. The fix is to mark one bean as @Primary or to use @Qualifier on the injection point.
For collections, Spring supports injection of all beans of a given type into a List<Interface>, which is incredibly useful for chain-of-responsibility patterns or multi-algorithm strategies.
Circular Dependencies: The Silent Startup Killer
A circular dependency occurs when Bean A depends on Bean B, and Bean B depends directly or indirectly on Bean A. Spring detects this during container initialization and throws BeanCurrentlyInCreationException. The fix is never to 'fix' the annotation; fix the design.
Three proven strategies to break circular dependencies: 1. Extract the shared logic into a third bean that both depend on (Mediator pattern). 2. Use setter injection with @Lazy on one side — this defers the creation of the lazy bean until it's actually needed, but beware: if you call a method on the lazy bean before its dependencies are resolved, you get a NullPointerException. 3. Redesign the architecture: circular dependencies almost always violate the Single Responsibility Principle. Maybe both beans should be merged, or an event-driven approach (ApplicationEventPublisher) would be cleaner.
In large legacy codebases, you'll often encounter indirect cycles (A → B → C → A). These are harder to spot. Use the startup debug log: 'spring.beaninfo.ignore=true' and check for 'Currently in creation' lines.
Performance Cost of Reflection-Based Injection
Spring's DI relies heavily on Java Reflection: scanning classes, inspecting constructors, fields, and annotations, and invoking methods reflectively. For startup, this is acceptable — the overhead is in the order of tens of milliseconds for a typical microservice. However, for request-scoped beans (prototype or request scope) that are created on every request, reflection-based field injection adds measurable latency.
Consider a prototype bean with 10 fields injected via @Autowired. Each field injection requires: field lookup by name, setting accessibility, and field.set() operation. That's ~200μs per prototype bean. Under 5000 requests/sec, that's an extra second per second of CPU time solely for injection.
Constructor injection avoids this because the container calls the constructor with resolved arguments using Constructor.newInstance() — a single reflective call for the entire bean. The difference is an order of magnitude for prototype-scoped beans.
Spring 6 and Spring Boot 3 have improved reflection caching, but the principle remains: constructor injection is not just cleaner design — it's measurably faster under load.
Field.set() calls. Switching to constructor injection dropped it to 1.5% and improved p99 latency by 3ms.Circular Dependency Causes Production Crash on Startup
- Never create circular constructor dependencies — they are a design smell, not a configuration problem.
- Use @Lazy only as a temporary escape hatch; it defers the problem and can cause NullPointerExceptions at runtime.
- Run a dependency graph analysis tool (e.g., IntelliJ Dependency Visualization) before every major release.
Key takeaways
Common mistakes to avoid
4 patternsMemorising syntax before understanding the concept
Skipping practice and only reading theory
Using field injection everywhere for convenience
Not understanding bean scopes and injection timing
Interview Questions on This Topic
What are the three types of dependency injection in Spring? When would you use each?
Frequently Asked Questions
That's Advanced Java. Mark it forged?
4 min read · try the examples if you haven't