🍃 Section 5 · Question #27

What to do if there are multiple beans of the same type

When the Spring IoC container finds multiple candidate beans of the same interface or class type during dependency injection, startup halts with a fatal exception: NoUniqueBeanD...


🟢 Junior Level

When the Spring IoC container finds multiple candidate beans of the same interface or class type during dependency injection, startup halts with a fatal exception: NoUniqueBeanDefinitionException: No qualifying bean of type 'PaymentService' available: expected single matching bean but found 2: cardPaymentService, paypalPaymentService.

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

@Component
public class CardPaymentService implements PaymentService { ... }

@Component
public class PaypalPaymentService implements PaymentService { ... }

The 3 Primary Disambiguation Solutions

1. @Primary (Designate a Global Default)

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

@Component
@Primary // Selected automatically if no @Qualifier is specified
public class CardPaymentService implements PaymentService { ... }

2. @Qualifier (Explicit Selection at Injection Point)

Explicitly specifies the exact bean name to inject:

@Service
public class CheckoutService {

    private final PaymentService paymentService;

    public CheckoutService(@Qualifier("paypalPaymentService") PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

3. Match byName (Parameter / Field Name Fallback)

If no qualifier is provided, Spring checks if the parameter or field name matches an existing bean name:

@Service
public class CheckoutService {

    // Matches the bean named "paypalPaymentService" as a fallback
    public CheckoutService(PaymentService paypalPaymentService) { ... }
}

🟡 Middle Level

Precedence Resolution Hierarchy

When resolving an ambiguous dependency across multiple candidate beans, Spring evaluates criteria in a strict hierarchical order:

1. @Qualifier (Explicit name or custom meta-qualifier — HIGHEST PRECEDENCE)
             │
             ├── Found -> ✅ Injected
             ▼
2. @Primary (Designates the default bean for this type)
             │
             ├── Found -> ✅ Injected
             ▼
3. @Priority (jakarta.annotation.Priority — Lowest integer value wins)
             │
             ├── Found -> ✅ Injected
             ▼
4. Fallback by Name (Matches parameter or field name against bean ID)
             │
             ├── Found -> ✅ Injected
             ▼
❌ NoUniqueBeanDefinitionException (Ambiguity cannot be resolved)

[!IMPORTANT] The @Qualifier Invariant: @Qualifier always overrides @Primary. If BeanA is marked with @Primary, but an injection point specifies @Qualifier("beanB"), Spring injects beanB. @Primary only takes effect when no qualifier is requested.

Implementing the Strategy Pattern via Map & List Injection

Instead of forcing the selection of a single bean, Spring allows you to inject all implementations of an interface simultaneously as a Map<String, T> or List<T>. This is the canonical way to implement the Strategy Pattern:

package com.example.service;

import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Map;

@Service
public class PaymentDispatcher {

    // Spring automatically populates this map:
    // Key (String)   = Bean name ("cardPaymentService", "paypalPaymentService")
    // Value (Object) = Bean instance
    private final Map<String, PaymentService> paymentServices;

    public PaymentDispatcher(Map<String, PaymentService> paymentServices) {
        this.paymentServices = paymentServices;
    }

    public void processPayment(String paymentMethod, BigDecimal amount) {
        PaymentService service = paymentServices.get(paymentMethod);
        if (service == null) {
            throw new IllegalArgumentException("Unsupported payment provider: " + paymentMethod);
        }
        service.pay(amount); // O(1) dynamic strategy dispatch without if-else or switch!
    }
}

🔴 Senior Level

Type-Safe Custom Qualifiers (Eliminating Fragile Strings)

Using raw string literals like @Qualifier("paypalPaymentService") introduces runtime fragility: typos are not caught by the compiler and fail only upon application startup.

The Solution: Meta-Qualifier Annotations

Create a custom annotation meta-annotated with @Qualifier:

package com.example.annotation;

import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier // Marks this annotation as a Spring Qualifier
public @interface PayPal {
}

Application on Bean and Injection Point:

// 1. Mark the Bean:
@Component
@PayPal
public class PaypalPaymentService implements PaymentService { ... }

// 2. Inject with Compile-Time Verification:
@Service
public class CheckoutService {

    private final PaymentService paymentService;

    public CheckoutService(@PayPal PaymentService paymentService) {
        this.paymentService = paymentService; // 100% type-safe and refactoring-resilient!
    }
}

Advanced Multi-Attribute Custom Qualifiers

Custom qualifiers can declare attributes (including Enums) for multi-dimensional disambiguation:

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface DatabaseRouting {
    StorageType value();
}

public enum StorageType { RELATIONAL, ANALYTICS, CACHE }
@Bean
@DatabaseRouting(StorageType.ANALYTICS)
public DataSource analyticsDataSource() { ... }

@Service
public class ReportingService {
    public ReportingService(@DatabaseRouting(StorageType.ANALYTICS) DataSource dataSource) { ... }
}

What Happens if Multiple Beans Have @Primary?

If two or more beans of the same type are both annotated with @Primary:

  1. Spring detects that ambiguity resolution via @Primary has failed.
  2. The container immediately throws: NoUniqueBeanDefinitionException: more than one 'primary' bean found among candidates: [cardPaymentService, cryptoPaymentService].
  3. To resolve this, remove @Primary from one candidate, use @Priority to establish an explicit ordering hierarchy, or switch to @Qualifier.

@Priority vs @Order Disambiguation Difference

  • jakarta.annotation.Priority: Recognized by Spring to disambiguate single bean injection points (when multiple candidates exist, the lowest integer value wins).
  • @Order / Ordered: Used exclusively for sorting elements inside collections (List<T>). @Order does not disambiguate single-target injection points!

4 Tricky Questions

1. What happens if two beans of the exact same type are both annotated with @Primary?

Answer: Spring will abort startup and throw a NoUniqueBeanDefinitionException: more than one 'primary' bean found among candidates: [beanA, beanB].

The @Primary annotation is designed to designate a unique, singular default bean. If multiple beans claim primary status, Spring treats this as a conflicting configuration and refuses to guess which default should take precedence.

2. Does @Primary override @Qualifier, or does @Qualifier override @Primary?

Answer: @Qualifier always overrides @Primary.

@Primary serves as a fallback default when an injection point requests a bean by type without specifying any qualifier. When an injection point explicitly declares @Qualifier("specificBean"), Spring narrows the search exclusively to beans bearing that qualifier, completely bypassing any beans marked with @Primary.

3. How can you implement the Strategy Pattern in Spring Boot using dependency injection without writing manual factory or switch statements?

Answer: By injecting a Map<String, StrategyInterface> into the consumer component.

Spring automatically detects that the injection target is a Map of beans implementing StrategyInterface. It populates the map with all available beans, where each map key is the bean’s registered name (beanName) and each value is the corresponding bean instance. At runtime, the dispatcher executes strategy lookup in $O(1)$ time via map.get(strategyKey).

4. Why and how should you create custom type-safe annotations with @Qualifier instead of using string literals?

Answer: Using string literals (@Qualifier("orderService")) is error-prone: typos cannot be detected by the Java compiler, and IDE refactoring tools will not automatically update strings when bean classes or methods are renamed.

To create a type-safe qualifier:

  1. Define a custom annotation and annotate it with Spring’s @Qualifier meta-annotation.
  2. Annotate the component class or @Bean method with your custom annotation.
  3. Annotate the constructor parameter or field at the injection point with your custom annotation. This provides full compile-time checking, IDE autocompletion, and refactoring safety.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

When multiple beans of the same type exist, Spring throws NoUniqueBeanDefinitionException. Resolving multiple beans:

  1. @Primary: Designates a default bean for general injection points.
  2. @Qualifier("name"): Explicitly targets a specific bean; always trumps @Primary.
  3. Custom Qualifiers: Meta-annotations with @Qualifier that provide type-safe, refactoring-resilient disambiguation without string typos.
  4. Map<String, T> Injection: Injects all matching beans for $O(1)$ Strategy Pattern dispatch.
  5. @Priority: Jakarta standard using integer ranking (lowest wins) to resolve single bean conflicts.

Resolution Priority Summary

Priority Mechanism Behavior
1 (Highest) @Qualifier / Custom Qualifier Explicitly targets matching qualifier
2 @Primary Chooses default bean if no qualifier is requested
3 @Priority Lowest integer priority wins
4 (Lowest) Fallback byName Matches parameter name to bean ID

Red Flags (DO NOT Say)

  • ❌ “@Primary overrides @Qualifier.” (@Qualifier has higher precedence and always wins).
  • ❌ “Injecting a Map<String, T> only injects the @Primary bean.” (It injects all registered beans of type T keyed by their bean IDs).
  • ❌ “@Order can be used to pick which single bean gets injected.” (@Order only controls sorting inside List<T>; it does not disambiguate single bean injection).
  • ❌ “You can have multiple @Primary beans for the same type to have multiple fallbacks.” (Multiple @Primary annotations throw NoUniqueBeanDefinitionException).