The error every Spring developer eventually hits
NoUniqueBeanDefinitionException: expected single matching bean but found 2. The moment you have
two implementations of an interface, Spring needs help choosing. Understanding its resolution
order turns this from a mysterious startup crash into a one-line fix — and it's a favorite
interview topic.
The resolution ladder
When Spring resolves an injection point, it walks a fixed sequence:
1. Match candidates BY TYPE
2. If several → narrow by @Primary
3. If still ambiguous → match @Qualifier value, or the bean NAME matching
the field/parameter name
4. If still none or many → throw NoSuchBean / NoUniqueBeanDefinitionException
Almost every wiring problem is a failure at one of these rungs: either no bean matched the type, or several did with no tie-breaker.
@Service
class Checkout {
Checkout(PaymentGateway gateway) { } // by type; fails if 0 or >1 candidates
}
@Qualifier: pick one explicitly
@Qualifier names the specific bean you want at a particular injection point:
@Component("stripe") class StripeGateway implements PaymentGateway { }
@Component("paypal") class PaypalGateway implements PaymentGateway { }
@Service
class Checkout {
Checkout(@Qualifier("stripe") PaymentGateway gateway) { } // pick Stripe
}
The string matches a bean name or a @Qualifier value declared on the bean itself. It works on
constructor params, fields, setters, and @Bean method parameters.
@Primary: set a default
@Primary chooses a global default among candidates, used whenever no qualifier is given:
@Component @Primary class StripeGateway implements PaymentGateway { } // default
@Component("paypal") class PaypalGateway implements PaymentGateway { }
@Service class NormalCheckout {
NormalCheckout(PaymentGateway g) { } // → Stripe
}
@Service class LegacyCheckout {
LegacyCheckout(@Qualifier("paypal") PaymentGateway g) { } // → PayPal, overrides @Primary
}
The rule: @Primary sets the baseline; @Qualifier overrides it where needed. Use @Primary
when one implementation is the "usual" choice; use @Qualifier for per-injection precision.
Fixing NoUniqueBeanDefinitionException
When you hit the ambiguity error, there are three fixes in order of preference:
@Component class JsonParser implements Parser { }
@Component class XmlParser implements Parser { }
@Service
class Importer {
Importer(Parser parser) { } // ✗ which one?
}
- Mark one bean
@Primaryif there's a sensible default. - Add
@Qualifier("xmlParser")at the injection point. - Inject all of them as
List<Parser>if you genuinely want every implementation.
The exception message lists the candidate bean names — read it to see exactly what collided.
Type-safe custom qualifiers
Stringly-typed @Qualifier("fast") works but isn't refactor-safe. Define a custom annotation
meta-annotated with @Qualifier:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE })
@interface Fast { }
@Component @Fast class InMemoryCache implements Cache { }
@Component class DiskCache implements Cache { }
@Service
class Lookup {
Lookup(@Fast Cache cache) { } // type-safe, compiler-checked
}
For value-based selection (region, tenant, tier), give the qualifier an attribute:
@Qualifier @Retention(RetentionPolicy.RUNTIME)
@interface Region { String value(); }
@Component @Region("eu") class EuDataSource implements DataSource2 { }
// Injected with: @Region("eu") DataSource2 ds
Spring matches the annotation type and its attribute values.
Generics are an implicit qualifier
Spring treats generic type arguments as part of the match, so distinct generics resolve without any qualifier:
interface Repository<T> { }
@Component class UserRepository implements Repository<User> { }
@Component class OrderRepository implements Repository<Order> { }
@Service
class UserService {
UserService(Repository<User> repo) { } // picks Repository<User> automatically
}
This removes a whole class of qualifier boilerplate in generic DAO/service hierarchies.
@Autowired vs @Resource vs @Inject
The three injection annotations differ mainly in their default matching strategy:
| Annotation | Origin | Matches by | Qualifier partner |
|---|---|---|---|
@Autowired | Spring | type, then qualifier/name | @Qualifier |
@Resource | Jakarta (JSR-250) | name, then type | name attribute |
@Inject | JSR-330 | type | @Named |
In Spring code, @Autowired + @Qualifier is idiomatic. @Inject + @Named is the portable,
standards-based equivalent. @Resource(name = "x") resolves by name first — handy for JSR-250
portability. Don't mix styles within one project.
ObjectProvider for zero/one/many
ObjectProvider<T> gracefully handles optional and ambiguous dependencies without startup failures:
@Service
class FlexibleService {
FlexibleService(ObjectProvider<MetricsExporter> exporters) {
exporters.ifAvailable(MetricsExporter::export); // safe if zero
var primary = exporters.getIfUnique(); // null if 0 or >1
exporters.orderedStream().forEach(MetricsExporter::export); // all, ordered
}
}
It resolves lazily and turns "zero/one/many" from exceptions into method calls.
Debugging injection problems
When the wrong bean (or none) is injected, read the exception's candidate list first, then confirm against the container:
String[] names = ctx.getBeanNamesForType(Parser.class); // see all candidates
// application.properties → debug=true
// GET /actuator/beans → every bean, its type, scope, dependencies
Most ambiguity bugs are "too many" (add @Primary/@Qualifier), "none" (missing stereotype), or
"not scanned" (bean outside the component-scan package).
Recap
Spring resolves injections by type, then @Primary, then @Qualifier/name, then fails. @Primary
sets a default; @Qualifier overrides per injection. Reach for type-safe custom qualifiers (with
attributes when selecting by value), lean on generics as an implicit qualifier, and use
ObjectProvider for optional or ambiguous dependencies. When it breaks, the exception message and
/actuator/beans tell you whether you have too many candidates, none, or one that was never
scanned.