Skip to content

Spring Boot · Dependency Injection

Resolving the Right Bean: @Qualifier, @Primary, and Spring's Resolution Order

5 min read Updated 2026-06-26 Share:

Practice Qualifiers & Resolution interview questions

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?
}
  1. Mark one bean @Primary if there's a sensible default.
  2. Add @Qualifier("xmlParser") at the injection point.
  3. 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:

AnnotationOriginMatches byQualifier partner
@AutowiredSpringtype, then qualifier/name@Qualifier
@ResourceJakarta (JSR-250)name, then typename attribute
@InjectJSR-330type@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.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel