Back to Dependency Injection
Q1Question 1 of 3

How do you resolve multiple beans of the same type?

Key Concepts
  • NoUniqueBeanDefinitionException occurs when Spring finds multiple beans of the same type and cannot decide which one to inject.
  • @Qualifier on the injection point explicitly names the bean to inject and has the highest priority.
  • @Primary on the bean definition marks it as the default when no qualifier is specified.
  • @Qualifier overrides @Primary: if both exist, the explicit qualifier wins.
  • List<T> injection gets all beans of type T; Map<String, T> injection maps bean names to instances for runtime selection.
  • Field or parameter name matching the bean name acts as an implicit qualifier.
  • Custom qualifier annotations like @PrimaryChannel provide type-safe, self-documenting disambiguation.

Definition

@Primary marks the default bean when no qualifier is specified; @Qualifier explicitly picks a specific bean by name; @Qualifier always overrides @Primary; List/Map injection gets all beans of the type.
  • 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 -- List injects all beans of type T; Map maps bean names to instances for runtime selection.
  • Custom qualifier -- a user-defined annotation like @PrimaryChannel that 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.

JAVA
package com.bank.service;

public interface NotificationService {
    void send(String accountId, String message);
}
JAVA
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);
    }
}
JAVA
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;
    }
}
TEXT
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.

JAVA
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:

JAVA
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);
    }
}
TEXT
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.

JAVA
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);
    }
}
TEXT
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 |

JAVA
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.

JAVA
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);
    }
}
TEXT
// 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:

JAVA
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 {}
JAVA
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);
    }
}
JAVA
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:

JAVA
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;
    }
}
TEXT
NoUniqueBeanDefinitionException: expected single matching bean but found 3

WRONG -- multiple @Primary on the same type:

JAVA
@Primary @Service
public class EmailNotificationService implements NotificationService { }

@Primary @Service // WRONG -- two @Primary
public class SmsNotificationService implements NotificationService { }
TEXT
NoUniqueBeanDefinitionException: more than one 'primary' bean found

CORRECT -- one @Primary, @Qualifier where needed:

JAVA
@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.

JAVA
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;
    }
}
PROPERTIES
# 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 @Qualifier on the method to give it a different qualifier name.
  • Ordered List injection. Beans in List are ordered by @Order annotation or Ordered interface. Use this to control processing order in a chain-of-responsibility pattern.
  • ObjectProvider for optional beans. ObjectProvider is lazy and null-safe. Use provider.getIfAvailable() when a bean may not exist.
  • Conditional beans. @ConditionalOnProperty, @ConditionalOnMissingBean provide more flexible alternatives to @Profile for feature flags and fallback beans.
  • Spring Boot auto-configuration uses @ConditionalOnMissingBean extensively: it defines a default bean, and your custom @Bean definition replaces it automatically.

What are common interview follow-up questions?

  • What happens if two beans have @Primary? (NoUniqueBeanDefinitionException.)
  • Can you use @Qualifier on a constructor parameter? (Yes, and this is the preferred approach with constructor injection.)
  • What is the difference between @Qualifier and 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? (List with @Order on each bean.)
  • Can @Profile and @Primary be combined? (Yes: @Profile("prod") @Primary marks the bean as default only when the prod profile is active.)

Visual Overview

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate