Definition
- NoUniqueBeanDefinitionException -- the exception Spring throws when it finds multiple beans of the same type and no mechanism resolves the ambiguity.
- @Qualifier -- placed on the injection point (constructor parameter, field, or setter) to explicitly name which bean to inject; highest priority.
- @Primary -- placed on the bean definition to mark it as the default when no qualifier is specified; lower priority than @Qualifier.
- List/Map injection --
Listinjects all beans of type T;Mapmaps bean names to instances for runtime selection. - Custom qualifier -- a user-defined annotation like
@PrimaryChannelthat provides type-safe disambiguation without relying on string bean names.
National Bank of India has three notification channels: email, SMS, and push. When the TransferService asks for "a NotificationService," Spring sees three candidates and panics (NoUniqueBeanDefinitionException). @Primary marks email as the default because most services want email. @Qualifier("sms") explicitly picks SMS for the fraud alert service that needs urgent delivery. List injection gets ALL channels for the marketing broadcaster that sends to every channel at once.
Understanding the Concepts
What is NoUniqueBeanDefinitionException?
When Spring finds multiple beans of the same type at an injection point and cannot determine which one to use, it throws NoUniqueBeanDefinitionException. This happens when there is no @Primary, no @Qualifier, and the parameter name does not match any bean name.
package com.bank.service;
public interface NotificationService {
void send(String accountId, String message);
}
package com.bank.service;
import org.springframework.stereotype.Service;
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String accountId, String message) {
System.out.println("Email to " + accountId + ": " + message);
}
}
@Service
public class SmsNotificationService implements NotificationService {
@Override
public void send(String accountId, String message) {
System.out.println("SMS to " + accountId + ": " + message);
}
}
@Service
public class PushNotificationService implements NotificationService {
@Override
public void send(String accountId, String message) {
System.out.println("Push to " + accountId + ": " + message);
}
}
package com.bank.service;
import org.springframework.stereotype.Service;
@Service
public class TransferService {
private final NotificationService notification; // three candidates!
public TransferService(NotificationService notification) {
this.notification = notification;
}
}
No qualifying bean of type 'NotificationService' available:
expected single matching bean but found 3:
emailNotificationService, smsNotificationService, pushNotificationService
How does the resolution order work?
Spring resolves ambiguity in this priority order:
| Priority | Mechanism | Where placed | Example |
|---|---|---|---|
| 1 (highest) | @Qualifier("name") | Injection point | @Qualifier("smsNotificationService") |
| 2 | @Primary | Bean definition | @Primary @Service |
| 3 | Parameter/field name | Injection point | NotificationService emailNotificationService |
| 4 | Single candidate | Automatic | Only one bean of the type exists |
If none of these resolves the ambiguity, Spring throws NoUniqueBeanDefinitionException.
How do you use @Primary?
@Primary goes on the bean definition (the class or @Bean method) and tells Spring: "When no qualifier is specified, use this one." It is the default fallback.
package com.bank.service;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
@Primary // default when no @Qualifier specified
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String accountId, String message) {
System.out.println("Email to " + accountId + ": " + message);
}
}
Now TransferService receives the email service automatically because it is marked @Primary:
package com.bank.service;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class TransferService {
private final NotificationService notification;
public TransferService(NotificationService notification) {
this.notification = notification; // gets EmailNotificationService (Primary)
}
public void transfer(String from, String to, BigDecimal amount) {
// ... transfer logic
notification.send(to, "Received " + amount + " from " + from);
}
}
Email to SAV2002: Received 10000 from SAV2001
How do you use @Qualifier?
@Qualifier goes on the injection point (constructor parameter, field, or setter) and explicitly names which bean to inject. It overrides @Primary.
package com.bank.service;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class FraudAlertService {
private final NotificationService notification;
// Explicitly picks SMS even though email is @Primary
public FraudAlertService(
@Qualifier("smsNotificationService") NotificationService notification) {
this.notification = notification;
}
public void alertFraud(String accountId, BigDecimal amount) {
notification.send(accountId,
"URGENT: Suspicious transaction of " + amount + " detected on account " + accountId);
}
}
SMS to SAV2001: URGENT: Suspicious transaction of 75000 detected on account SAV2001
How does @Primary differ from @Qualifier?
| Aspect | @Primary | @Qualifier |
|---|---|---|
| Placed on | Bean definition (class or @Bean method) | Injection point (parameter, field, setter) |
| Meaning | "I am the default" | "I want this specific one" |
| Priority | Lower | Higher (overrides @Primary) |
| Count | Only one per type (multiple = error) | Any number per type |
| Use case | Most consumers want the same bean | Specific consumer needs a different bean |
package com.bank.service;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
// @Primary on the bean definition
@Primary
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String accountId, String message) {
System.out.println("Email: " + message);
}
}
// Consumer with NO qualifier: gets @Primary (email)
@Service
public class TransferService {
public TransferService(NotificationService notification) {
// notification = EmailNotificationService
}
}
// Consumer WITH qualifier: gets SMS (overrides @Primary)
@Service
public class FraudAlertService {
public FraudAlertService(
@Qualifier("smsNotificationService") NotificationService notification) {
// notification = SmsNotificationService
}
}
How does List and Map injection work?
List injects all beans of type T. Map maps bean names to instances, enabling runtime selection without if-else chains.
package com.bank.service;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class MarketingBroadcastService {
private final List<NotificationService> allChannels;
public MarketingBroadcastService(List<NotificationService> allChannels) {
this.allChannels = allChannels;
}
// Broadcasts to ALL channels
public void broadcastPromotion(String accountId, String promo) {
allChannels.forEach(ch -> ch.send(accountId, promo));
}
}
@Service
public class CustomerPreferenceService {
private final Map<String, NotificationService> channelMap;
public CustomerPreferenceService(Map<String, NotificationService> channelMap) {
this.channelMap = channelMap;
}
// Selects channel at runtime based on customer preference
public void notifyByPreference(String accountId, String message, String preferred) {
NotificationService channel = channelMap.get(preferred);
if (channel == null) {
channel = channelMap.get("emailNotificationService"); // fallback
}
channel.send(accountId, message);
}
}
// MarketingBroadcastService.broadcastPromotion("SAV2001", "50% off on locker rental")
Email to SAV2001: 50% off on locker rental
SMS to SAV2001: 50% off on locker rental
Push to SAV2001: 50% off on locker rental
// CustomerPreferenceService.notifyByPreference("SAV2001", "EMI due", "smsNotificationService")
SMS to SAV2001: EMI due
The Map injection pattern is powerful for runtime dispatch. Adding a new channel (WhatsApp, for example) requires only creating a new @Service class. No existing code changes are needed.
How do custom qualifier annotations work?
String-based @Qualifier("name") is fragile because a typo compiles but fails at runtime. Custom qualifier annotations provide type-safe disambiguation:
package com.bank.qualifier;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface PrimaryChannel {}
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface UrgentChannel {}
package com.bank.service;
import com.bank.qualifier.PrimaryChannel;
import com.bank.qualifier.UrgentChannel;
import org.springframework.stereotype.Service;
@PrimaryChannel
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void send(String accountId, String message) {
System.out.println("Email: " + message);
}
}
@UrgentChannel
@Service
public class SmsNotificationService implements NotificationService {
@Override
public void send(String accountId, String message) {
System.out.println("SMS: " + message);
}
}
package com.bank.service;
import com.bank.qualifier.UrgentChannel;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class FraudAlertService {
private final NotificationService notification;
public FraudAlertService(@UrgentChannel NotificationService notification) {
this.notification = notification; // type-safe, no string
}
}
A typo in @UrgentChannel is a compile error. A typo in @Qualifier("smsNotificationService") only fails at runtime.
What are common mistakes with bean disambiguation?
WRONG -- forgetting to resolve ambiguity:
package com.bank.service;
import org.springframework.stereotype.Service;
@Service
public class LoanService {
private final NotificationService notification;
// WRONG -- three candidates, no qualifier, no primary
public LoanService(NotificationService notification) {
this.notification = notification;
}
}
NoUniqueBeanDefinitionException: expected single matching bean but found 3
WRONG -- multiple @Primary on the same type:
@Primary @Service
public class EmailNotificationService implements NotificationService { }
@Primary @Service // WRONG -- two @Primary
public class SmsNotificationService implements NotificationService { }
NoUniqueBeanDefinitionException: more than one 'primary' bean found
CORRECT -- one @Primary, @Qualifier where needed:
@Primary @Service // default
public class EmailNotificationService implements NotificationService { }
@Service // no @Primary
public class SmsNotificationService implements NotificationService { }
@Service
public class LoanService {
public LoanService(NotificationService notification) {
// gets EmailNotificationService (Primary)
}
}
@Service
public class FraudAlertService {
public FraudAlertService(
@Qualifier("smsNotificationService") NotificationService notification) {
// gets SmsNotificationService (Qualifier overrides Primary)
}
}
What about @Profile for environment-specific beans?
@Profile activates beans based on the environment. Only beans matching the active profile are registered in the container.
package com.bank.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource h2DataSource() {
// In-memory H2 for development
return new org.h2.jdbcx.JdbcDataSource();
}
@Bean
@Profile("prod")
public DataSource postgresDataSource() {
// PostgreSQL for production
var ds = new com.zaxxer.hikari.HikariDataSource();
ds.setJdbcUrl("jdbc:postgresql://db.nbi.com:5432/banking");
return ds;
}
}
# application.properties
spring.profiles.active=prod
With @Profile("prod") active, only postgresDataSource is registered. There is no ambiguity because h2DataSource does not exist in the container.
What should a senior engineer know?
- @Qualifier with @Bean methods. When defining beans with
@Bean, the method name becomes the bean name. Use@Qualifieron the method to give it a different qualifier name. - Ordered List injection. Beans in
Listare ordered by@Orderannotation orOrderedinterface. Use this to control processing order in a chain-of-responsibility pattern. - ObjectProvider for optional beans.
ObjectProvideris lazy and null-safe. Useprovider.getIfAvailable()when a bean may not exist. - Conditional beans.
@ConditionalOnProperty,@ConditionalOnMissingBeanprovide more flexible alternatives to@Profilefor feature flags and fallback beans. - Spring Boot auto-configuration uses
@ConditionalOnMissingBeanextensively: it defines a default bean, and your custom@Beandefinition replaces it automatically.
What are common interview follow-up questions?
- What happens if two beans have
@Primary? (NoUniqueBeanDefinitionException.) - Can you use
@Qualifieron a constructor parameter? (Yes, and this is the preferred approach with constructor injection.) - What is the difference between
@Qualifierand parameter name matching? (Qualifier is explicit and has higher priority; name matching is implicit and fragile.) - How do you inject all beans of a type in a specific order? (
Listwith@Orderon each bean.) - Can
@Profileand@Primarybe combined? (Yes:@Profile("prod") @Primarymarks the bean as default only when the prod profile is active.)
Visual Overview
Resolution Priority (highest to lowest):
┌────────────────────────────────────────┐
│ 1. @Qualifier("beanName") │ explicit pick
│ │ │
│ ▼ │
│ 2. @Primary │ default fallback
│ │ │
│ ▼ │
│ 3. Parameter/field name match │ implicit match
│ │ │
│ ▼ │
│ 4. Single candidate │ only one exists
│ │ │
│ ▼ │
│ NONE → NoUniqueBeanDefinitionException│
└────────────────────────────────────────┘
Injection Patterns:
TransferService ──→ @Primary (email) │ default
FraudAlertService ─→ @Qualifier (sms) │ explicit
MarketingService ──→ List<all channels> │ broadcast
PreferenceService ─→ Map<name, channel> │ runtime pick
Key Takeaway
When Spring finds multiple beans of the same type, it throws NoUniqueBeanDefinitionException unless you tell it which one to use. The resolution mechanisms in priority order are: @Qualifier on the injection point which explicitly names the bean, @Primary on the bean definition which marks the default, parameter or field name matching a bean name as an implicit qualifier, and single-candidate resolution when only one bean exists. @Qualifier always overrides @Primary.
In the National Bank of India notification gateway, Rajesh has three NotificationService implementations: email (marked @Primary because most services want email), SMS, and push. The TransferService receives the @Primary email bean automatically for transfer confirmations. The FraudAlertService uses @Qualifier("smsNotificationService") to explicitly request SMS for urgent security alerts that need immediate delivery. The MarketingBroadcastService injects a List of all NotificationService beans to broadcast promotions through every channel simultaneously. The CustomerPreferenceService injects a Map keyed by bean name and selects the channel at runtime based on Priya's saved preference in the database, avoiding if-else chains entirely.
Adding a new notification channel, such as WhatsApp, requires only creating a new @Service class that implements NotificationService. The List and Map injections pick it up automatically. No existing code changes are needed. For type-safe disambiguation without fragile string names, Pankaj recommends custom qualifier annotations like @PrimaryChannel and @UrgentChannel, where a typo is a compile error instead of a runtime failure.
Remember one sentence: @Primary marks the default bean, @Qualifier explicitly picks a specific one, and @Qualifier always wins when both are present.
---