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 Dependency Injection interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.