Skip to main content

2.5 Disambiguation & Qualifiers

← Back to Chapter 2: Spring Beans & Dependency Injection

When multiple beans implement the same interface, Spring's autowiring by type encounters ambiguity (NoUniqueBeanDefinitionException). Spring provides precise annotations to disambiguate and prioritize candidates.


1. The Disambiguation Problem​

Consider two payment gateway implementations:

public interface PaymentGateway { void pay(BigDecimal amount); }

@Component("stripe")
public class StripeGateway implements PaymentGateway { ... }

@Component("paypal")
public class PaypalGateway implements PaymentGateway { ... }

@Service
public class CheckoutService {
// FAILS AT STARTUP: NoUniqueBeanDefinitionException: expected single matching bean but found 2: stripe, paypal
public CheckoutService(PaymentGateway paymentGateway) { ... }
}

2. Solutions: @Primary vs @Qualifier​

A. @Primary (Default Priority)​

Designates one candidate as the default choice when no specific qualifier is requested:

@Component
@Primary
public class StripeGateway implements PaymentGateway { ... }

Now CheckoutService(PaymentGateway gateway) automatically receives StripeGateway.

B. @Qualifier (Explicit Target Selection)​

Pinpoints the exact bean by identifier or custom qualifier name:

@Service
public class CheckoutService {
private final PaymentGateway paymentGateway;

public CheckoutService(@Qualifier("paypal") PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}

3. Custom Qualifier Annotations​

String qualifiers (@Qualifier("paypal")) can be error-prone due to typos. Modern Spring projects define custom qualifier annotations:

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface StripeProvider {}

// Apply to implementation
@Component
@StripeProvider
public class StripeGateway implements PaymentGateway { ... }

// Apply to injection point
@Service
public class CheckoutService {
public CheckoutService(@StripeProvider PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
}

4. Injecting Collections of Beans & @Order​

If you want to invoke all implementations of an interface (e.g., executing a chain of validators), inject a List<T> or Map<String, T>:

@Service
public class ValidationPipeline {
private final List<Validator> validators;

// Spring injects all Validator beans sorted by @Order
public ValidationPipeline(List<Validator> validators) {
this.validators = validators;
}

public void validateAll(Order order) {
validators.forEach(v -> v.validate(order));
}
}

@Component
@Order(1) // Runs first
public class SecurityValidator implements Validator { ... }

@Component
@Order(2) // Runs second
public class InventoryValidator implements Validator { ... }