Dependency Injection & Bean Lifecycle

@Autowired, @Qualifier & @Primary

18 min Lesson 4 of 13

@Autowired, @Qualifier & @Primary

Spring's dependency injection becomes powerful the moment you stop manually wiring beans and let the framework resolve them automatically. The three annotations covered in this lesson — @Autowired, @Qualifier, and @Primary — are the core tools Spring provides for that resolution. Understanding not just how to use them, but when and why, will save you from the most common DI-related bugs and keep your configuration clean at scale.

How Spring Resolves a Dependency

When Spring encounters a dependency injection point — a constructor parameter, a setter, or a field — it follows a two-step lookup:

  1. Type match: find all beans in the application context whose type is compatible with the required type.
  2. Name match (tiebreaker): if exactly one candidate remains, use it. If multiple candidates remain, try matching the injection point's variable name to a bean name. If still ambiguous, throw NoUniqueBeanDefinitionException.

The annotations in this lesson give you explicit control over each step of that process.

@Autowired — Marking an Injection Point

@Autowired (from org.springframework.beans.factory.annotation) tells Spring: fill this dependency from the context. You can place it on a constructor, a setter method, or directly on a field.

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderService { private final PaymentGateway paymentGateway; private NotificationSender notificationSender; // Constructor injection (preferred — field is final, easy to test) @Autowired public OrderService(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; } // Setter injection (optional dependency — Spring calls this only if a bean exists) @Autowired(required = false) public void setNotificationSender(NotificationSender notificationSender) { this.notificationSender = notificationSender; } }
Constructor injection omits @Autowired in modern Spring. Since Spring 4.3, if a class has exactly one constructor Spring injects it automatically — no annotation required. You will see this pattern everywhere in Spring Boot codebases: a @Service class with a single constructor and no @Autowired at all.

The required = false attribute is important: it tells Spring not to fail startup if no matching bean is found. Use it for truly optional integrations (analytics, feature-flag providers) — never as a way to silence a missing mandatory dependency.

The Ambiguity Problem

As soon as you have more than one bean of the same type in the context, Spring cannot decide which one to inject and throws NoUniqueBeanDefinitionException. This is extremely common with interfaces:

public interface PaymentGateway { void charge(long amountCents, String currency); } @Component public class StripeGateway implements PaymentGateway { ... } @Component public class PayPalGateway implements PaymentGateway { ... }

Now injecting PaymentGateway anywhere is ambiguous. Spring finds two candidates and has no rule to choose between them. You have two tools to fix this: @Primary and @Qualifier.

@Primary — Designating the Default

@Primary marks one bean as the preferred candidate whenever its type is required but no explicit qualifier has been given. Think of it as setting the system-wide default.

import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @Component @Primary public class StripeGateway implements PaymentGateway { @Override public void charge(long amountCents, String currency) { // Stripe SDK call } } @Component public class PayPalGateway implements PaymentGateway { @Override public void charge(long amountCents, String currency) { // PayPal SDK call } }

Now any injection point that asks for a PaymentGateway — without further qualification — receives the StripeGateway:

@Service public class OrderService { private final PaymentGateway paymentGateway; // gets StripeGateway public OrderService(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; } }
@Primary is for the common case, not the only case. It works best when one implementation covers 80–90% of all injection sites and only a handful of places need a specific alternative. If every consumer needs a different implementation, @Primary provides false clarity — use @Qualifier throughout instead.

@Qualifier — Precise, Per-Site Selection

@Qualifier attaches a string label to a bean and requires that exact label at the injection point, bypassing the type-match ambiguity entirely. The label defaults to the class name with a lowercase first letter (the bean's default name), or you can set it explicitly.

import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component @Qualifier("stripe") public class StripeGateway implements PaymentGateway { ... } @Component @Qualifier("paypal") public class PayPalGateway implements PaymentGateway { ... }

At the injection point you repeat the qualifier:

@Service public class RefundService { private final PaymentGateway gateway; // Refunds must go through PayPal per the business contract public RefundService(@Qualifier("paypal") PaymentGateway gateway) { this.gateway = gateway; } }
Qualifier strings are stringly-typed magic literals. A typo silently becomes a "no bean found" error at startup — not at compile time. Teams working at scale often define qualifier constants or, better yet, create a custom qualifier annotation to get compile-time safety and IDE navigation.

Custom Qualifier Annotations — The Production Pattern

A custom qualifier annotation eliminates the string-literal problem entirely. You define a meta-annotated annotation once and use it as a type-safe qualifier everywhere:

import org.springframework.beans.factory.annotation.Qualifier; import java.lang.annotation.*; @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface PayPalPayment {}
@Component @PayPalPayment public class PayPalGateway implements PaymentGateway { ... } // Usage at injection site public RefundService(@PayPalPayment PaymentGateway gateway) { this.gateway = gateway; }

Now the compiler enforces the contract. If @PayPalPayment is renamed or removed, every usage site fails to compile immediately.

Combining @Primary and @Qualifier

These two annotations work together predictably: @Qualifier always wins over @Primary. If a bean is marked @Primary but the injection point carries a @Qualifier, Spring ignores the primary designation and uses the qualifier to select the bean.

  • No qualifier at injection site → use @Primary bean (if one exists), else fail with ambiguity.
  • Qualifier present → match by qualifier, ignore @Primary entirely.
  • Neither → Spring falls back to matching the variable name against bean names (fragile; avoid relying on this).

Injecting All Candidates — Collections and Optional

Sometimes you want every implementation — for example, to fan out a notification to all registered channels. Spring handles this automatically when you inject a List or Map:

import java.util.List; @Service public class NotificationDispatcher { private final List<NotificationSender> senders; // Spring injects ALL beans that implement NotificationSender public NotificationDispatcher(List<NotificationSender> senders) { this.senders = senders; } public void broadcast(String message) { senders.forEach(s -> s.send(message)); } }

Injecting Map<String, NotificationSender> gives you the bean name as the key — handy when you need to select an implementation at runtime by name.

Summary

Use @Autowired (or simply a single-constructor class) to declare injection points. When multiple candidates of the same type exist, resolve the ambiguity with @Primary for a single default bean, or @Qualifier for per-site precision. Prefer custom qualifier annotations over string literals in any codebase you expect to maintain. And remember: @Qualifier always takes precedence over @Primary.