Skip to content

Qualifiers & Resolution Interview Questions & Answers

15 questions Updated 2026-06-26 Share:

How Spring resolves which bean to inject when several match — @Qualifier, @Primary, custom qualifiers, @Resource, by-type vs by-name resolution, and how NoUniqueBeanDefinitionException is fixed.

Read the in-depth guideResolving the Right Bean: @Qualifier, @Primary, and Spring's Resolution Order(opens in new tab)
15 of 15

Spring's default resolution is by type: it finds all beans assignable to the injection point's type. If exactly one matches, it's injected. Ambiguity (more than one) triggers a tie-breaking sequence before it gives up.

@Service
class Checkout {
    // Spring looks for a single bean assignable to PaymentGateway:
    Checkout(PaymentGateway gateway) { } // ← by type
}

The full algorithm: (1) match candidates by type; (2) if several, narrow by @Primary; (3) if still ambiguous, match the @Qualifier value or fall back to the bean name matching the parameter/field name; (4) if still none or many, throw NoUniqueBeanDefinitionException / NoSuchBeanDefinitionException. Understanding this ladder is the key to fixing most wiring errors.

Rule of thumb: Resolution is type → @Primary → @Qualifier/name → error. When it fails, you're either missing a bean or have several of one type with no tie-breaker.

@Qualifier disambiguates which bean to inject when several beans share a type. You give the target injection point a name (or custom qualifier) that picks one specific candidate.

interface PaymentGateway { }
@Component("stripe") class StripeGateway implements PaymentGateway { }
@Component("paypal") class PaypalGateway implements PaymentGateway { }

@Service
class Checkout {
    Checkout(@Qualifier("stripe") PaymentGateway gateway) {  // pick Stripe explicitly
        // ...
    }
}

Without the qualifier, two PaymentGateway beans would cause NoUniqueBeanDefinitionException. @Qualifier("stripe") tells Spring exactly which one. It works on constructor params, fields, setters, and @Bean method parameters. The string matches a bean name or a value declared via @Qualifier on the bean itself.

Rule of thumb: When more than one bean fits a type, name the one you want with @Qualifier at the injection point — it's per-injection precision.

Use @Primary to set a single global default among candidates; use @Qualifier to choose a specific bean at a particular injection point. They're complementary, and @Qualifier overrides @Primary.

@Component @Primary class StripeGateway implements PaymentGateway { }  // default
@Component("paypal") class PaypalGateway implements PaymentGateway { }

@Service class NormalCheckout {
    NormalCheckout(PaymentGateway g) { }                  // → Stripe (the @Primary)
}
@Service class LegacyCheckout {
    LegacyCheckout(@Qualifier("paypal") PaymentGateway g) { } // → PayPal (overrides)
}

Reach for @Primary when one implementation is the "usual" choice and others are exceptions — most injection points just want the default. Reach for @Qualifier when there's no obvious default, or when a specific consumer needs a non-default bean. Together: @Primary sets the baseline, @Qualifier overrides it where needed.

Rule of thumb: @Primary = one default for everyone; @Qualifier = pick-this-one here. @Qualifier always wins over @Primary at that injection point.

NoUniqueBeanDefinitionException means Spring matched more than one bean for a by-type injection and has no tie-breaker. It's one of the most common Spring startup errors.

// Two beans of the same type, no @Primary, no @Qualifier → ambiguous:
@Component class JsonParser implements Parser { }
@Component class XmlParser  implements Parser { }

@Service
class Importer {
    Importer(Parser parser) { }   // ✗ which Parser? → NoUniqueBeanDefinitionException
}

Three fixes, in order of preference: (1) mark one bean @Primary if there's a sensible default; (2) add @Qualifier("xmlParser") at the injection point to name the one you want; (3) inject all of them as a List<Parser> if you genuinely want every implementation. The error message lists the candidate bean names — read it to see what collided.

Rule of thumb: "No unique bean" = multiple matches with no tie-breaker. Add @Primary for a default, @Qualifier to choose one, or inject a List to take them all.

Instead of stringly-typed @Qualifier("name"), you can define your own type-safe qualifier annotation meta-annotated with @Qualifier. It documents intent and survives refactoring/renames.

@Qualifier                                   // meta-annotated → acts as a qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE })
public @interface Fast { }

@Component @Fast class InMemoryCache implements Cache { }
@Component       class DiskCache     implements Cache { }

@Service
class Lookup {
    Lookup(@Fast Cache cache) { }            // type-safe: injects InMemoryCache
}

The custom annotation behaves exactly like @Qualifier("...") but is checked by the compiler and refactor-safe — no magic strings to mistype. This is the cleanest approach when you have a recurring distinction (fast/slow, primary/replica, internal/external) used in many places.

Rule of thumb: For qualifiers you reuse across the codebase, make a custom @Qualifier-meta-annotated annotation — type safety and IDE support beat magic strings.

A custom qualifier can declare attributes, letting one annotation type select among many beans by value — a richer alternative to many separate marker annotations.

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE })
public @interface Region {
    String value();                          // the attribute
}

@Component @Region("us") class UsDataSource implements DataSource2 { }
@Component @Region("eu") class EuDataSource implements DataSource2 { }

@Service
class EuService {
    EuService(@Region("eu") DataSource2 ds) { }   // matches @Region("eu")
}

Spring matches the annotation type and all its attribute values between the bean and the injection point. This scales better than defining @UsRegion, @EuRegion, … as separate annotations — you parameterize one. Useful for multi-tenant, multi-region, or replica selection wiring.

Rule of thumb: Give a custom qualifier attributes when the distinction is a value (region, tenant, tier) rather than a fixed category — one annotation, many parameterized matches.

For @Bean-defined beans the method name is the bean name and thus the default qualifier, but you can add an explicit @Qualifier (or a custom qualifier) on the method to decouple the qualifier from the name.

@Configuration
class DataConfig {
    @Bean
    @Qualifier("readOnly")               // qualifier independent of method name
    DataSource readReplica() { return ...; }

    @Bean
    @Primary
    DataSource primary() { return ...; }
}

@Service
class Reporting {
    Reporting(@Qualifier("readOnly") DataSource ds) { } // gets readReplica()
}

You can qualify by the method name (@Qualifier("readReplica")) or by an explicit @Qualifier/custom annotation placed on the method. The latter is handy when you want a stable qualifier that won't change if you rename the method. Combine with @Primary to set a default plus named alternates.

Rule of thumb: Qualify @Bean methods with an explicit @Qualifier on the method when you want the qualifier to survive method renames; otherwise the method name works as the qualifier.

@Autowired (Spring) resolves by type first, then narrows by qualifier/name. @Resource (Jakarta/JSR-250) resolves by name first, then falls back to type. The default matching strategy is the core difference.

@Service
class Demo {
    @Autowired
    @Qualifier("stripe")
    private PaymentGateway a;          // by type, disambiguated by qualifier

    @Resource(name = "stripe")
    private PaymentGateway b;          // by name "stripe" directly
}

With @Autowired, type drives the match and @Qualifier is the tie-breaker. With @Resource, the name drives the match — @Resource(name = "x") or the field name — and type is the fallback. In Spring projects @Autowired + @Qualifier is idiomatic; @Resource is more common in code aiming for Jakarta-EE portability.

Rule of thumb: @Autowired = by-type (+ @Qualifier); @Resource = by-name. Stick with @Autowired/@Qualifier in Spring code unless you specifically need JSR-250 by-name semantics.

@Inject (from jakarta.inject / JSR-330) is the standard equivalent of Spring's @Autowired. Spring supports it natively, so code using @Inject is portable across DI containers (Spring, CDI, Guice).

// Spring-specific:
@Autowired
@Qualifier("stripe")
PaymentGateway a;

// JSR-330 standard equivalent:
@Inject
@Named("stripe")          // @Named is the JSR-330 qualifier
PaymentGateway b;

Mappings: @Inject ≈ @Autowired, @Named ≈ @Qualifier, @Singleton ≈ singleton scope. The main behavioral difference is that @Inject has no required attribute (it's always required; use Optional/Provider for optionality). Choosing between them is mostly about whether you value Spring features (required = false) or framework-agnostic portability.

Rule of thumb: @Inject + @Named is the portable, standards-based pair; @Autowired + @Qualifier is the Spring-native pair with extras like required = false. Don't mix them in one project.

When you inject List<T>, the order isn't guaranteed by declaration — control it explicitly with @Order (or implementing Ordered). Lower values come first.

@Component @Order(1) class AuthFilter   implements Filter2 { }
@Component @Order(2) class LoggingFilter implements Filter2 { }
@Component @Order(3) class CacheFilter   implements Filter2 { }

@Service
class Pipeline {
    private final List<Filter2> filters;          // ordered: Auth, Logging, Cache
    Pipeline(List<Filter2> filters) { this.filters = filters; }
}

@Order matters whenever the collection represents a pipeline or chain where sequence is semantically important (filters, validators, interceptors). Without it, the order may follow bean-definition/scan order, which is fragile. Ordered.HIGHEST_PRECEDENCE and LOWEST_PRECEDENCE give you the extremes. Note @Order affects collection injection order, not bean creation order (use @DependsOn for that).

Rule of thumb: Annotate chain members with @Order so an injected List runs them in a defined sequence; don't rely on incidental scan order for pipelines.

ObjectProvider<T> is a smarter injection handle that defers resolution and gracefully handles zero, one, or many candidates — avoiding both startup failures and ambiguity exceptions.

@Service
class FlexibleService {
    private final ObjectProvider<MetricsExporter> exporters;
    FlexibleService(ObjectProvider<MetricsExporter> exporters) { this.exporters = exporters; }

    void run() {
        exporters.ifAvailable(MetricsExporter::export);     // safe if zero beans
        MetricsExporter primary = exporters.getIfUnique();  // null if 0 or >1
        exporters.orderedStream().forEach(MetricsExporter::export); // all, ordered
    }
}

Unlike a direct dependency, ObjectProvider doesn't fail at startup when the bean is missing, and unlike a raw List it offers getIfAvailable, getIfUnique, and lazy getObject(). It also resolves lazily — the bean isn't fetched until you call the provider. It's the Swiss-army tool for "this dependency might be absent, ambiguous, or expensive."

Rule of thumb: Use ObjectProvider<T> when a dependency is optional, possibly ambiguous, or should be resolved lazily — it turns "zero/one/many" from exceptions into method calls.

Spring treats generic type arguments as part of the injection match, so two beans of the same raw type but different generics resolve unambiguously — no @Qualifier needed.

interface Repository<T> { }
@Component class UserRepository  implements Repository<User>  { }
@Component class OrderRepository implements Repository<Order> { }

@Service
class UserService {
    // Resolved by the generic argument — picks Repository<User> automatically:
    UserService(Repository<User> repo) { }
}

Even though both beans are Repository, the generic parameter (<User> vs <Order>) makes the injection point specific. Spring retains and matches these type arguments. This is especially clean for generic DAO/service hierarchies, where it removes a whole class of qualifier boilerplate.

Rule of thumb: Distinct generic arguments are themselves a qualifier — Repository<User> and Repository<Order> inject unambiguously without any @Qualifier.

Yes — placing @Qualifier on a List<T> (or using a custom qualifier on a group of beans) injects only the matching subset, not every bean of the type. It's a way to group beans.

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@interface Critical { }

@Component @Critical class FraudCheck implements Validator2 { }
@Component @Critical class LimitCheck implements Validator2 { }
@Component            class StyleCheck implements Validator2 { }

@Service
class CriticalPipeline {
    // Only the @Critical-qualified validators, not StyleCheck:
    CriticalPipeline(@Critical List<Validator2> validators) { }
}

Without the qualifier, List<Validator2> would collect all three. The qualifier on the list filters to just the beans carrying the matching qualifier, letting you maintain several named groups of the same interface and inject whichever group a consumer needs.

Rule of thumb: Qualify a List<T> to inject a named subset of implementations — handy when you keep multiple groups (critical vs optional, inbound vs outbound) of one interface.

@Fallback (Spring Framework 6.2+) marks a bean as the candidate to use only when no non-fallback bean matches — the inverse of @Primary. It's the "default of last resort."

@Component @Fallback                     // used only if nothing better exists
class NoOpMailSender implements MailSender { }

// If the app also defines a real SmtpMailSender (non-fallback), THAT wins;
// the @Fallback bean is ignored when a regular candidate is present.
@Component
class SmtpMailSender implements MailSender { }   // takes precedence over @Fallback

@Primary says "prefer me over the others." @Fallback says "ignore me if there's anyone else." It's ideal for library/auto-config defaults that should quietly step aside the moment the application supplies its own implementation — similar in spirit to @ConditionalOnMissingBean but expressed as a resolution preference rather than a condition.

Rule of thumb: @Primary = preferred default; @Fallback = last-resort default that yields to any real implementation. Use @Fallback for stand-in beans in shared libraries.

When injection misbehaves, read the exception message first — Spring names the expected type and the candidate beans — then inspect the container's actual bean list.

// 1. The startup error itself lists candidates, e.g.:
// "expected single matching bean but found 2: jsonParser, xmlParser"

// 2. Enumerate what the container actually holds:
String[] names = ctx.getBeanNamesForType(Parser.class);   // see all candidates

// 3. Turn on the auto-config/condition report to see what got created:
// application.properties → debug=true

// 4. Inspect at runtime via Actuator (needs actuator starter):
// GET /actuator/beans   → every bean, its type, scope, and dependencies

Most ambiguity bugs are: two implementations with no @Primary/@Qualifier, a missing stereotype so the bean was never created, or a bean defined outside the component-scan package. /actuator/beans and getBeanNamesForType quickly reveal which of these it is — far faster than guessing.

Rule of thumb: Read the exception's candidate list, then confirm with /actuator/beans or getBeanNamesForType — injection bugs are almost always "too many," "none," or "not scanned."

More ways to practice

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

Join our WhatsApp Channel