Databases, Spring and microservices · 2. Spring Core and Spring MVC, lesson 2 of 8

Dependency injection in depth: constructor, setter, field, @Qualifier and @Primary

Intermediate3 min read@since 17Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

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 call new. With a single constructor, @Autowired is optional.
  • Setter injection: for genuinely optional dependencies that can change later.
  • Field injection (@Autowired on a field): short, but it hides dependencies, prevents final and needs reflection in tests. Avoid it outside tests.

When several beans match:

  • @Primary marks the default choice.
  • @Qualifier("name") on the parameter picks one explicitly, and beats @Primary.
  • Ask for all of them: List<PaymentGateway> or Map<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.

Spring container lab

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.

Spring container lab

Optional dependencies and collections

Not every dependency must exist:

  • Optional<Notifier> or ObjectProvider<Notifier>.getIfAvailable() when a bean may be missing.
  • List<Validator> receives every matching bean (ordered by @Order); an empty list if there are none.
  • ObjectProvider also defers lookup until you need it, which helps with expensive or scoped beans.

Example

Java
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

Part of Spring Core and Spring Security in depth.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.