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
Lesson 8 of 14
1d streakThe 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:
- 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.
- 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.
- 03Step 3 (Primary Designation): If ambiguity still exists, check if exactly one candidate is annotated with @Primary. If so, that candidate wins.
- 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.
@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.
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) {}
}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
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
@Primary
- 2
@Qualifier
- 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.
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));
}
}
}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
Key Takeaway
Spring resolves injection candidates via a deterministic 4-step algorithm (Type → Qualifier → Primary → Name) and enables extensible plugin architectures via collection and map injection.
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
Lesson Primary References
Supplementary Module Resources