Dependency injection in depth: constructor, setter, field, @Qualifier and @Primary
Spring injects dependencies by type: a constructor parameter of type PaymentGateway gets the bean that implements it.
- Constructor injection (recommended): every dependency is visible in one place, fields can be
final, the object is never half-built, and tests can just callnew. With a single constructor,@Autowiredis optional. - Setter injection: for genuinely optional dependencies that can change later.
- Field injection (
@Autowiredon a field): short, but it hides dependencies, preventsfinaland needs reflection in tests. Avoid it outside tests.
When several beans match:
@Primarymarks the default choice.@Qualifier("name")on the parameter picks one explicitly, and beats@Primary.- Ask for all of them:
List<PaymentGateway>orMap<String, PaymentGateway>(keyed by bean name), which is a clean way to implement the Strategy pattern.
Optional dependencies: Optional<T>, ObjectProvider<T> or @Autowired(required = false).
Circular dependencies (A needs B, B needs A) fail at startup in Spring Boot. Fix the design: extract the shared part into a third bean, or decouple with events. @Lazy works around it, but hides the problem.
Circular dependencies
If A needs B and B needs A, neither can be constructed first. Spring Boot (since 2.6) refuses to start rather than half-solving it. The cycle usually means the two classes share a responsibility that belongs in a third class, or that one of them should react to an event instead of calling the other. The lab shows the cycle being detected, and both ways out.
Optional dependencies and collections
Not every dependency must exist:
Optional<Notifier>orObjectProvider<Notifier>.getIfAvailable()when a bean may be missing.List<Validator>receives every matching bean (ordered by@Order); an empty list if there are none.ObjectProvideralso defers lookup until you need it, which helps with expensive or scoped beans.
Example
public interface PaymentGateway {
PaymentResult charge(Order order);
}
@Component("razorpay")
class RazorpayGateway implements PaymentGateway { /* … */ }
@Component("stripe")
@Primary // the default when nobody says otherwise
class StripeGateway implements PaymentGateway { /* … */ }
@Service
public class CheckoutService {
private final PaymentGateway defaultGateway;
private final PaymentGateway upiGateway;
private final Map<String, PaymentGateway> gateways;
public CheckoutService(PaymentGateway defaultGateway, // @Primary: stripe
@Qualifier("razorpay") PaymentGateway upiGateway, // explicit choice
Map<String, PaymentGateway> gateways) { // all of them, by bean name
this.defaultGateway = defaultGateway;
this.upiGateway = upiGateway;
this.gateways = gateways;
}
public PaymentResult pay(Order order, String provider) {
return gateways.getOrDefault(provider, defaultGateway).charge(order); // Strategy pattern
}
}Common mistake
Field injection with @Autowired: unit tests that call new get null dependencies and NullPointerExceptions, and the class can quietly grow ten dependencies without anyone noticing.
Under the hood
How Spring picks a candidate: match by type; if several match, a qualifier on the injection point decides, then @Primary, then a bean whose name matches the parameter name; otherwise NoUniqueBeanDefinitionException. With Lombok, @RequiredArgsConstructor writes the constructor for final fields. Constructor injection also makes circular dependencies impossible to hide: they fail fast at startup instead of surfacing as subtle bugs.
Check yourself
Two PaymentGateway beans: stripe is @Primary, and the constructor parameter has @Qualifier("razorpay"). Which is injected?
How this connects
Know these first
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.