Dependency Injection in Java — Circular Refs That Crash
BeanCurrentlyInCreationException kills startup when two beans inject each other via constructor.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- 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.Why Injecting Interfaces is the Only Safe Choice in Production
Most tutorials show you how to inject a concrete class and call it DI. That's not DI. That's fancy instantiation. Real dependency injection only works when you program to interfaces. Why? Because concrete classes chain you to their implementation details. Swap a JDBC connection pool? Now you're rewriting every injection point. Stub a service for integration tests? Can't, if the constructor expects a concrete MySqlPaymentGateway. The interface is your contract. The implementation is just a detail you can replace without touching consumers. In production, your DI container resolves the interface to a concrete bean based on qualifiers, profiles, or even runtime conditions. That's the whole point of “Don’t call us, we’ll call you.” Your code should never, ever instantiate its own dependencies. If I see new PaymentGateway() inside a service class during a code review, that PR gets rejected on the spot. Every class that needs a collaborator should declare that need via an interface in its constructor or setter. That keeps your system decoupled, testable, and deployable without a rewrite every time a vendor changes their API.
OrderService is now coupled to StripeGateway. When your boss asks to switch to Adyen, every test and production path breaks.Constructor Injection is Production-Ready; Field Injection is Technical Debt
You see it everywhere: @Autowired slapped on a private field. Clean, concise, wrong. Field injection hides dependencies. Your class looks like it has none until Spring calls new through reflection, leaving the field null until the proxy is fully constructed. That means you can’t test it without the container. Constructor injection makes dependencies explicit. Every constructor parameter is a dependency your class admits it needs. No surprises. No null pointer exceptions because the container missed a field. The class is immutable after construction and always in a valid state. In production, your build tool (SpotBugs, Checkstyle) can verify that every field is final and set in the constructor. Field injection defeats static analysis. Use it in throwaway prototypes if you must. In production code, you’re asking for runtime failures that compile-time checks would have caught. Spring’s own docs recommend constructor injection. When you see a colleague using field injection, send them here. It’s not style. It’s correctness.
Scoping: How Singletons, Prototypes, and Request Scopes Actually Behave Under Load
Every DI framework defaults to singleton scoping. That means one bean instance lives for the entire application context. It’s fast and thread-safe if the bean is stateless. But throw a stateful bean into a singleton scope and you’re debugging race conditions at 3 AM. Prototype scope creates a new instance every time you request a bean. Period. Useful for objects that hold request-specific state. But if a singleton bean injects a prototype bean, guess what? That prototype is instantiated once when the singleton is created, not when you call a method. The container only creates new prototypes when you call applicationContext.getBean() or use @Lookup or javax.inject.Provider. In a web app, request and session scopes keep beans alive for the duration of an HTTP request or user session. These rely on proxy mode (scoped-proxy="target-class" in XML or @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)). Without the proxy, a singleton that injects a request-scoped bean will hold a stale instance. I’ve seen that bug sink a production deployment. Know your scope lifetimes. Over-scope to singleton, you share state. Under-scope to prototype, you leak memory. Pick the right scope for the job.
Stop Wiring by Hand: How to Build a Custom Injector That Won't Embarrass You in Production
Your team is wasting time wiring objects manually or cargo-culting Spring annotations without understanding what happens underneath. That's how you get startups that fail silently at 3 AM. You need to know how to build a minimal DI container from scratch—not to replace Spring, but to understand why Spring works the way it does.
The core mechanic is simple: scan constructors, resolve dependencies recursively, cache singletons. Stop pretending this is magic. A production-grade injector does exactly three things: reflect on constructor parameters, walk the dependency graph depth-first, and throw a hard error on cycles before startup completes.
Here's a bare-bones injector that handles the critical path. No qualifiers, no scopes—just raw dependency resolution with cycle detection. This is the skeleton your tools wrap in annotations and XML. Understand this, and you stop fighting your framework.
Stop Guessing: Profile the Cost of Reflection Before It Hits Production
Every time you let Spring scan another package or deploy another prototype bean, you're adding startup tax. New devs treat this as free. It's not. Reflection-based constructor resolution costs roughly 0.5–2 µs per bean on modern JVMs—that's fine for 200 beans, but disastrous for 2,000 with mixed scopes.
The real killer? Prototype scope. Each getBean() call re-resolves constructors reflectively. If your request-scoped controller injects a prototype service, that's a reflection hit per request. Under 10k RPM, those microseconds compound into seconds of CPU time wasted on metadata parsing.
Here's a benchmark that proves the gap. Run this against your own code before you argue with me. Measure the difference between direct instantiation and reflective instantiation. If your service layer takes more than 5ms to inject under load, you have a scaling problem that no amount of caching fixes.
-Dsun.reflect.inflationThreshold=0 and add --add-opens java.base/java.lang=ALL-UNNAMED to let the JIT inline reflective calls after warmup. This cuts reflection overhead by ~70% after the first 15 invocations per constructor.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.
grep -r "@Component" io/thecodeforge/repository/Verify component-scan path in @SpringBootApplication annotation.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
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Advanced Java. Mark it forged?
8 min read · try the examples if you haven't