Foundation · Understand

Resolving Ambiguity

When several beans share one type, resolve the conflict deliberately with @Primary, @Qualifier, parameter-name matching, or collection injection.

Primary source: Spring Framework 6.2 Reference: Fine-tuning Autowiring with Qualifiers

20 min

Lesson 8 of 14

1d streak

The Architectural Question

Startup failure is the container refusing to guess; the fix belongs in the wiring, not in a try/catch around getBean.

What this guide proves

  • @Primary
  • @Qualifier
  • List<T> Injection

The 4-step autowiring candidate resolution algorithm

When Spring encounters an injection point (such as a constructor parameter), DefaultListableBeanFactory.doResolveDependency() executes a deterministic 4-step candidate resolution algorithm to find the matching bean:

  1. 01Step 1 (Type Matching): Find all bean definitions whose target type is assignable to the requested parameter type (including generic type checks). If exactly 1 bean matches, return it immediately.
  2. 02Step 2 (Qualifier Filtering): If multiple beans match the type, filter the candidate set by @Qualifier metadata. Any candidate whose qualifier matches the injection point qualifier is retained.
  3. 03Step 3 (Primary Designation): If ambiguity still exists, check if exactly one candidate is annotated with @Primary. If so, that candidate wins.
  4. 04Step 4 (Name Fallback): If ambiguity persists, Spring checks if one of the candidate bean names matches the constructor parameter or field name.

If after Step 4 multiple candidates still remain, Spring aborts startup immediately with a NoUniqueBeanDefinitionException.

Strategy patterns with collection & map injection

Spring provides built-in support for the Strategy design pattern: injecting collections (List<T>, Set<T>) or maps (Map<String, T>) automatically gathers all matching beans in the container.

NotificationDispatcher.java
@Component
public class NotificationDispatcher {
    // Map key is the bean name ("emailSender", "smsSender"); value is the instance
    private final Map<String, MessageSender> senders;

    // List is ordered according to @Order or PriorityOrdered annotations
    private final List<MessageFilter> filters;

    public NotificationDispatcher(Map<String, MessageSender> senders, List<MessageFilter> filters) {
        this.senders = Map.copyOf(senders);
        this.filters = List.copyOf(filters);
    }

    public void dispatch(String channel, String message) {
        MessageSender sender = Optional.ofNullable(senders.get(channel))
            .orElseThrow(() -> new IllegalArgumentException("Unknown channel: " + channel));
        sender.send(message);
    }
}

Break it deliberately: The NoUniqueBeanDefinitionException

Declaring multiple implementations of an interface without specifying @Qualifier or @Primary triggers an immediate startup failure.

AmbiguousBeans.java
public interface PaymentGateway {}

@Component("stripe")
public class StripeGateway implements PaymentGateway {}

@Component("paypal")
public class PayPalGateway implements PaymentGateway {}

@Service
public class CheckoutService {
    // Ambiguous: 2 candidates match PaymentGateway!
    public CheckoutService(PaymentGateway gateway) {}
}
Terminal / Diagnostic Output
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.example.CheckoutService required a single bean, but 2 were found:
	- stripe: defined in file [StripeGateway.class]
	- paypal: defined in file [PayPalGateway.class]

Action:
Consider marking one of the beans as @Primary, updating the consumer to qualify which bean is needed (via @Qualifier), or using a List/Map.

Resolution strategy

Use @Primary when one implementation is the natural default for 90% of use cases. Use @Qualifier("stripe") when the caller specifically requires that provider. Inject Map<String, PaymentGateway> when the choice is dynamic at runtime.

Check your understanding

Active Recall Checkpoint

What is the exact evaluation order when Spring resolves an ambiguous dependency among multiple candidate beans?

Hint: Recall the 4-step algorithm.

Show answer & explanation

Spring evaluates candidates in this order: (1) Type compatibility, (2) @Qualifier metadata match, (3) @Primary designation, and (4) Parameter/Field name fallback.

Active Recall Checkpoint

When injecting a Map<String, StrategyInterface>, what does Spring populate as the Map key and value?

Hint: Think about bean naming.

Show answer & explanation

Spring populates the bean name (String) as the key, and the container-managed singleton instance as the value.

Runtime Architecture & Flow

Resolution model

Configuration resolution checklist

  1. 1

    @Primary

  2. 2

    @Qualifier

  3. 3

    List<T> Injection

Inspect the effective application state before assuming which declaration won.

Working Implementation

Code Walkthrough

Inject a `List<PaymentStrategy>` into a service to implement the Strategy Pattern, then use `@Primary` to set a default.

AmbiguityResolutionLab.java
package com.example.core;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import java.util.Map;

public class AmbiguityResolutionLab {
    public interface NotificationSender {
        void send(String recipient, String message);
    }

    @Component("emailSender")
    @Primary // Default candidate when no qualifier is specified
    public static class EmailSender implements NotificationSender {
        public void send(String r, String m) { System.out.println("Email -> " + r + ": " + m); }
    }

    @Component("smsSender")
    public static class SmsSender implements NotificationSender {
        public void send(String r, String m) { System.out.println("SMS -> " + r + ": " + m); }
    }

    @Component
    public static class AlertDispatcher {
        private final NotificationSender urgentSender;
        private final Map<String, NotificationSender> allSenders;

        // 1. @Qualifier explicitly selects candidate bean by name
        // 2. Map<String, T> injects all beans of the type keyed by bean name
        public AlertDispatcher(
                @Qualifier("smsSender") NotificationSender urgentSender,
                Map<String, NotificationSender> allSenders) {
            this.urgentSender = urgentSender;
            this.allSenders = allSenders;
        }

        public void dispatchUrgent(String target, String msg) {
            urgentSender.send(target, msg);
        }

        public void dispatchAll(String target, String msg) {
            allSenders.forEach((name, sender) -> sender.send(target, "[" + name + "] " + msg));
        }
    }
}
Multiple beans of the same interface cause NoUniqueBeanDefinitionException unless resolved explicitly.
@Primary sets a default choice; @Qualifier at the injection site takes highest precedence.
Injecting Map<String, T> or List<T> enables the Strategy and Plugin pattern without hardcoding bean names.

Synthesis & Production Verification

Production Acceptance Criteria

  • Resolve an ambiguous injection with @Primary and separately with @Qualifier
  • Inject all implementations as a List or Map keyed by bean name
  • Match a bean by parameter name without any qualifier annotation

Architectural & Technical Review

Core Interview Questions

Q01What is the 4-step candidate resolution algorithm executed by Spring's DefaultListableBeanFactory?Show answer

Spring resolves dependencies by: (1) Type compatibility, (2) @Qualifier metadata match, (3) @Primary designation, and (4) Parameter or field name fallback.

Q02How does Spring populate injected Map<String, ServiceInterface> and List<ServiceInterface> collections?Show answer

Spring collects all matching bean definitions in the container. For maps, the bean name is the key and the instance is the value. For lists, instances are ordered by @Order or PriorityOrdered annotations.

Q03What causes a NoUniqueBeanDefinitionException and what are the standard ways to resolve it?Show answer

Multiple beans match the injection point type without disambiguation. It is resolved by annotating one candidate with @Primary, using @Qualifier at the injection point, or injecting a collection/map.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Coupling business code to hardcoded bean names — Relying on parameter name matching rather than explicit @Qualifier or @Primary makes code fragile to refactoring or compiler parameter name stripping.
  • Pitfall: Assuming collection injection order is deterministic without @Order — Spring collection injection order is unpredictable unless candidate beans explicitly declare @Order or implement Ordered.

Primary Documentation & Specifications

Supplementary Module Resources