Dependency Injection in Depth
Constructor, setter, and field injection; resolving ambiguity with @Qualifier and @Primary; and why constructor injection wins.
On this page
The container wires objects together through dependency injection (DI) - it injects each bean's collaborators instead of the bean fetching them. Spring supports three styles, but they are not equal. Choosing well is one of the clearest marks of a professional Spring developer.
The three styles
// 1. CONSTRUCTOR injection - the recommended default
@Service
class BookService {
private final BookRepository repository;
BookService(BookRepository repository) { // Spring passes the bean in
this.repository = repository;
}
}
// 2. SETTER injection - for genuinely optional dependencies
@Service
class BookService {
private NotificationClient notifier;
@Autowired(required = false)
void setNotifier(NotificationClient notifier) { this.notifier = notifier; }
}
// 3. FIELD injection - concise, but avoid it (see below)
@Service
class BookService {
@Autowired private BookRepository repository;
}You can drop @Autowired on constructors
If a bean has a single constructor, Spring uses it automatically - no
@Autowired needed. That's why constructor injection reads so cleanly.
Why constructor injection wins
Prefer constructor injection for required dependencies. It gives you three things the others can't:
finalfields & immutability - the dependency is set once and never null.- Fail-fast wiring - a missing dependency is caught the instant the container
starts, not at first use with a
NullPointerException. - Trivial testing - you construct the object with plain
newin a unit test and pass a mock, no Spring required.
A constructor that lists every dependency is a recipe that names all its ingredients up front: read it once and you know exactly what the dish requires - and you can't start cooking while an ingredient is missing. Field injection is a recipe that says "grab whatever you need from the pantry as you go" - you only discover a missing ingredient halfway through, at runtime.
Resolving ambiguity
What if two beans could satisfy one dependency? Say two PaymentGateway
implementations. Spring can't guess, so it fails - unless you disambiguate:
@Component
@Primary // the default choice when ambiguous
class StripeGateway implements PaymentGateway { }
@Component("legacyGateway")
class PaypalGateway implements PaymentGateway { }
@Service
class Checkout {
Checkout(@Qualifier("legacyGateway") PaymentGateway gateway) { // pick explicitly
// ...
}
}@Primarymarks one bean as the default winner.@Qualifier("name")picks a specific bean by name.
You can even inject all implementations at once - Spring collects every
matching bean into a List or Map:
@Service
class NotificationDispatcher {
private final List<NotificationChannel> channels; // every channel bean, injected
NotificationDispatcher(List<NotificationChannel> channels) {
this.channels = channels;
}
}This is a beautifully clean way to implement the Strategy pattern - add a new
NotificationChannel bean and it joins the list automatically, no registration.
Here is a class using field injection:
@Service
class BookService {
@Autowired private BookRepository repository;
List<Book> featured() { return repository.findFeatured(); }
}Rewrite it with constructor injection, then write the one line that constructs it in a unit test with a mock repository (no Spring context).
Why is constructor injection preferred over field injection for required dependencies?
Key takeaways
- Dependency injection means the container supplies a bean's collaborators; Spring offers constructor, setter, and field injection.
- Prefer constructor injection for required dependencies: it enables final fields, fails fast on missing beans, and makes unit testing trivial.
- A single constructor needs no @Autowired annotation.
- Disambiguate multiple candidates with @Primary (default winner) or @Qualifier (pick by name).
- Injecting a List or Map of an interface gives you every implementing bean - a clean, registration-free Strategy pattern.