Spring provides multiple ways to resolve dependency injection conflicts when multiple beans of the same type exist. Two commonly used annotations to handle this are @Primary
and @Qualifier
.
Let’s dive into the difference between them, their usage with examples, and best practices.
🧠 Problem Scenario
Let’s say you have an interface MessageService
and two implementations: EmailService
and SMSService
.
public interface MessageService {
void sendMessage(String message);
}
@Component
public class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending Email: " + message);
}
}
@Component
public class SMSService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
Now, when Spring tries to autowire MessageService
, it doesn’t know whether to inject EmailService
or SMSService
—ambiguity arises.
✅ Solution 1: @Primary
– Mark One as the Default
Use @Primary
when one implementation should be the default.
@Component
@Primary
public class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending Email: " + message);
}
}
@Component
public class SMSService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
@Component
public class NotificationSender {
private final MessageService messageService;
@Autowired
public NotificationSender(MessageService messageService) {
this.messageService = messageService;
}
public void send(String message) {
messageService.sendMessage(message);
}
}
🔍 Output:
Sending Email: Hello from @Primary
✅ Solution 2: @Qualifier
– Be Explicit
Use @Qualifier
when you want to be specific about which implementation to inject.
@Component("emailService")
public class EmailService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending Email: " + message);
}
}
@Component("smsService")
public class SMSService implements MessageService {
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
Now inject with @Qualifier
:
@Component
public class NotificationSender {
private final MessageService messageService;
@Autowired
public NotificationSender(@Qualifier("smsService") MessageService messageService) {
this.messageService = messageService;
}
public void send(String message) {
messageService.sendMessage(message);
}
}
🔍 Output:
Sending SMS: Hello from @Qualifier
⚖️ @Primary
vs @Qualifier
– When to Use What?
Criteria | @Primary |
@Qualifier |
---|---|---|
Default choice | ✅ Yes | ❌ No |
Explicit selection | ❌ No | ✅ Yes |
Suitable for | One most-used implementation | Multiple specific implementations |
Use Case | App has a preferred/default bean | You switch between multiple beans frequently |
Can be overridden? | ✅ Yes, with @Qualifier
|
✅ Always explicit |
💡 Best Practices
- Use
@Primary
only when one implementation should be used in most cases. -
Use
@Qualifier
when:- You need fine-grained control.
- You inject different implementations in different places.
Avoid combining both unless necessary (e.g., default with override).
🧪 Bonus: Use with @Bean
methods too!
@Configuration
public class AppConfig {
@Bean
@Primary
public MessageService emailService() {
return new EmailService();
}
@Bean
public MessageService smsService() {
return new SMSService();
}
}
You can use the same @Primary
and @Qualifier("smsService")
approach with @Bean
methods.
🏁 Conclusion
- Use
@Primary
to define the default bean when multiple candidates exist. - Use
@Qualifier
when you need precise control. - They’re not mutually exclusive—use both smartly based on your project needs.