Start Learning
Javaneer
Back to stage
Module 0·Spring Foundations

Dependency Injection in Depth

Constructor, setter, and field injection; resolving ambiguity with @Qualifier and @Primary; and why constructor injection wins.

18 min readIntermediate
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:

  • final fields & 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 new in a unit test and pass a mock, no Spring required.
Constructor injection is a complete recipe card

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
        // ...
    }
}
  • @Primary marks 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.

Make it testable

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.
Was this lesson helpful?
Edit this page on GitHub