Java & Spring Boot
Java & Spring Boot/senior/freq 5/5

Dependency Injection in Spring

Spring's IoC container wires collaborators at startup. Prefer constructor injection for required dependencies — it enables immutability, testability, and fail-fast wiring.

springiocbeans

Deep dive

What it really is

Spring builds a graph of singleton beans during ApplicationContext startup. Each bean's dependencies are resolved by type (and disambiguated by @Qualifier or bean name). Constructor injection is the only style that makes a bean mandatory and immutable.

Why constructor over field

  • Detects cycles and missing beans at startup, not at first call.
  • Lets you mark fields final and instantiate the bean in tests with new.
  • Plays nicely with @RequiredArgsConstructor (Lombok) and Java records.

Scopes

singleton (default), prototype, request, session. Misusing prototype inside singleton requires ObjectProvider or @Lookup — otherwise the prototype is captured once and never re-created.

Real-world example

From production

Migrating a 200k-LOC monolith from field injection to constructor injection surfaced 14 circular dependencies that had silently worked because Spring lazily resolved fields. Three of them were latent NPEs hit only at peak load. The refactor itself was mechanical; the value was the surfaced design debt.

Interview questions

3 senior-level
Q1Field vs constructor injection — which and why?

Constructor for required collaborators, setter for optional reconfiguration, never field outside tests. Constructor enables immutability, fails fast on missing beans, exposes cycles, and lets the class be instantiated without Spring in unit tests.

Q2How does Spring resolve two beans of the same type?

By name (parameter name matches bean name), @Qualifier, @Primary, or @Profile. If still ambiguous, startup fails with NoUniqueBeanDefinitionException. I prefer @Qualifier with a constant — it's explicit and refactor-safe.

Q3Why is injecting a prototype bean into a singleton risky?

The prototype is resolved once at wiring time and cached, defeating the scope. Fix with ObjectProvider<T>, Provider<T>, or method injection via @Lookup.

Common mistakes

  • Using @Autowired on fields, then wondering why tests need a full context.

  • Hiding cycles with @Lazy instead of fixing the design.

  • Component-scanning the entire com package, slowing startup and pulling in test fixtures.

Trade-offs

  • Constructor injection forces you to deal with cycles upfront — that's a feature, not a bug.

  • Heavy use of @Conditional makes context startup behaviour environment-dependent; document it.

Related