Have you ever had your app refuse to start the moment you registered two implementation classes for the same interface, like payment methods or notification channels? The DI container throws a NoUniqueBeanDefinitionException, effectively saying “I don’t know which one to inject.” Let’s work through why this exception happens, how to choose between @Qualifier and @Primary, and what the priority order is when both are in play — all with concrete example code.
Why Does NoUniqueBeanDefinitionException Occur?
Let’s register two implementations — credit card and bank transfer — as @Component beans for a PaymentService interface.
public interface PaymentService {
void pay(int amount);
}
@Component
public class CreditCardPaymentService implements PaymentService {
@Override
public void pay(int amount) {
System.out.println("Paying by credit card: " + amount);
}
}
@Component
public class BankTransferPaymentService implements PaymentService {
@Override
public void pay(int amount) {
System.out.println("Paying by bank transfer: " + amount);
}
}
If you simply use @Autowired as-is, an exception occurs at startup.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService; // Startup error occurs here
}
The DI container finds two beans of type PaymentService, and the basic handling of beans registered with @Component alone isn’t enough to determine which one should be injected.
Identifying the Cause from the Error Message
The exception displayed in this case looks like this:
org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.example.PaymentService' available:
expected single matching bean but found 2: creditCardPaymentService,bankTransferPaymentService
The names listed are the default bean names, which lowercase the first letter of the class name. Having two candidates listed is a sign that “writing code to explicitly choose one will resolve this,” so from here, we’ll use @Qualifier and @Primary to communicate our intent.
Explicitly Specifying the Injection Target with @Qualifier
First, give each implementation class an identifying name.
@Component
@Qualifier("creditCard")
public class CreditCardPaymentService implements PaymentService {
// ...
}
@Component
@Qualifier("bankTransfer")
public class BankTransferPaymentService implements PaymentService {
// ...
}
By specifying the same name with @Qualifier on the injection side, only the intended implementation gets injected.
@Service
public class OrderService {
@Autowired
@Qualifier("creditCard")
private PaymentService paymentService;
}
If the specified identifier doesn’t match any implementation, the number of candidates becomes zero, so instead of NoUniqueBeanDefinitionException, you get a different exception: NoSuchBeanDefinitionException (no matching qualifier value). It’s worth remembering that “too many candidates” and “no candidates found” are distinct errors — this helps when reading logs.
Using @Qualifier with Constructor Injection
Just as with @Qualifier, constructor injection is recommended over field injection. You can attach the annotation directly to the parameter.
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(@Qualifier("creditCard") PaymentService paymentService) {
this.paymentService = paymentService;
}
}
Since the dependency is visible as a parameter, swapping in a mock for testing is as simple as calling new OrderService(mockPaymentService).
Specifying a Default Implementation with @Primary
If writing @Qualifier every time feels redundant, there’s an approach where you attach @Primary to the implementation that should be used by default.
@Component
@Primary
public class CreditCardPaymentService implements PaymentService {
// ...
}
With just this, CreditCardPaymentService will be selected without any changes to the earlier OrderService code (the field injection without @Qualifier).
Scope and Caveats of @Primary
The effect of @Primary isn’t limited to “the one place where the annotation was added” — it applies to “every location in the application that injects that type.” If there are 10 classes injecting PaymentService, CreditCardPaymentService becomes the default at all 10 locations.
If you want to use a different implementation at just a few specific locations, you can override it by specifying @Qualifier only at those locations. Also, if you attach @Primary to multiple classes, then “which Primary to use” becomes ambiguous again, and the same NoUniqueBeanDefinitionException will recur. The basic rule is to have at most one @Primary per type.
Priority When @Qualifier and @Primary Conflict
It’s fine to have implementations with @Primary and implementations with @Qualifier coexisting. If the injection side has @Qualifier, that takes priority; if not, @Primary is used.
@Component
@Primary
public class CreditCardPaymentService implements PaymentService { /* ... */ }
@Component
@Qualifier("bankTransfer")
public class BankTransferPaymentService implements PaymentService { /* ... */ }
@Service
public class OrderService {
@Autowired
private PaymentService defaultPayment; // The @Primary credit card implementation
@Autowired
@Qualifier("bankTransfer")
private PaymentService alternativePayment; // Explicit specification takes priority
}
The key point is that even for the same type, you can decide which one to use on a per-injection-point basis.
Fallback Resolution by Parameter Name to Bean Name
Even without @Qualifier or @Primary, if the field name or constructor parameter name matches a bean name, Spring will automatically use that to resolve the dependency.
@Service
public class OrderService {
@Autowired
private PaymentService creditCardPaymentService; // Resolved by name match
}
While convenient, the moment you rename a variable during refactoring, resolution breaks and the exception reappears at startup. In code maintained by a team, it’s safer to be explicit with @Qualifier rather than relying on name-based resolution.
Summary of Priority Order
The resolution methods covered so far have a clear priority order.
@Qualifierspecified on the injection side@Primary- Bean name resolution via field name or parameter name
If none of these apply and multiple candidates remain, you get NoUniqueBeanDefinitionException. In team development, establishing a convention of “decide the default with @Primary, and override only the exceptional cases with @Qualifier” lets new team members understand the intent just by reading the code.
Switching Implementations via Configuration or Profiles
If you don’t want to hardcode the payment method and instead want to switch it via configuration, you can use @ConditionalOnProperty.
payment:
default-method: credit-card
@Component
@ConditionalOnProperty(name = "payment.default-method", havingValue = "credit-card")
public class CreditCardPaymentService implements PaymentService { /* ... */ }
@Component
@ConditionalOnProperty(name = "payment.default-method", havingValue = "bank-transfer")
public class BankTransferPaymentService implements PaymentService { /* ... */ }
Since the havingValue conditions are mutually exclusive, only one of the beans gets registered in the container at startup. Because there’s only ever a single candidate to begin with, @Primary isn’t needed.
If you want to use a mock in your development environment and an actual API client in production, @Profile works on the same principle.
@Component
@Profile("dev")
public class MockPaymentGatewayService implements PaymentService { /* ... */ }
@Component
@Profile("prod")
public class RealPaymentGatewayService implements PaymentService { /* ... */ }
Simply specifying something like spring.profiles.active=dev registers only the bean corresponding to the active profile, so the correct implementation gets selected for each environment.
Conclusion
NoUniqueBeanDefinitionException is simply an error that occurs because you haven’t told the DI container “which implementation to use.” If you establish a basic policy of deciding the default implementation with @Primary and overriding only specific locations with @Qualifier, you can safely handle multiple implementations on top of the fundamental concept of DI. Furthermore, combining this with @ConditionalOnProperty or profiles makes it straightforward to build a configuration that switches implementations based on environment or settings.