PaymentGateway and calls its constructor parameter paypalGateway; the application has two gateway beans. Compile the class with one compiler flag and the application starts and injects the PayPal gateway. Compile the same source without the flag and it refuses to start with expected single matching bean but found 2. Nothing in the source changed, and there is no annotation that says which build you are in.
That behaviour is the last rung of a ladder Spring climbs every time it has to choose between beans, and most explanations of @Autowired stop at the first rung. This article walks the whole ladder with a small payment-gateway example, prints the real exception for each way of getting it wrong, and then covers the injection points that take several beans at once: List, Map, Optional and ObjectProvider. Every code block links to a file in the core-di module of a companion repository that compiles and runs, and every console block is quoted from a transcript that test run wrote, not typed in by hand.
Versions. Spring Boot 4.1.1 and Spring Framework 7.0.9 (both poms were published to Maven Central on 20 August 2026), on Java 25 (LTS, Temurin 25.0.4.1). The scenarios run on a plain Spring ApplicationContext, so only the container’s own rules are in play. If you have not read it, the previous article on constructor, setter and field injection explains how the handed-over bean reaches your class; this one is about which bean gets handed over.
@Autowired asks one question first: which beans have this type?
Spring keeps a catalogue of every bean it has built or can build. When a class asks for a collaborator — with@Autowired, or simply as a constructor parameter, as covered in the previous article — the container does not go looking for a bean by a name you gave. It looks at the type of the parameter and collects every bean assignable to it. What happens next depends only on how many it found.
/** Asks for a PaymentGateway and says nothing more. */
public class CheckoutPlain {
private final PaymentGateway gateway;
public CheckoutPlain(PaymentGateway gateway) {
this.gateway = gateway;
}
public String gatewayClass() {
return gateway.getClass().getSimpleName();
}
}
Register only that class, with no gateway bean at all, and the top branch of the diagram plays out. 12-missing-bean.txt is the transcript:
top exception : org.springframework.beans.factory.UnsatisfiedDependencyException
top message : Error creating bean with name 'checkoutPlain': Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
root cause : org.springframework.beans.factory.NoSuchBeanDefinitionException
root message : No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
The fingerprint of the zero-candidate case.expected at least 1 bean which qualifies as autowire candidateat the bottom, wrapped in anUnsatisfiedDependencyExceptionwhose message names the consumer (checkoutPlain) and the exact parameter (constructor parameter 0). The last part,Dependency annotations: {}, lists the annotations Spring saw on the injection point; empty means there was no@Qualifieror anything else narrowing the request. Read that line first when you are unsure whether a qualifier is being applied.
Going deeper: reading the exception chain
Both transcripts in this section print two exceptions: the top one, which is what Spring throws out of refresh(), and the root cause underneath. The top one, UnsatisfiedDependencyException, tells you where the problem is — which bean, which constructor parameter — and the root cause tells you what is wrong. In a Spring Boot application the same chain is turned into the formatted “APPLICATION FAILED TO START” report, so the two pieces of information are still there, only laid out differently.
The two root exceptions are related: NoUniqueBeanDefinitionException extends NoSuchBeanDefinitionException (checked with javap against spring-beans-7.0.9.jar). A catch (NoSuchBeanDefinitionException e) therefore catches both “none” and “too many”, which is worth remembering the first time a fallback path swallows the wrong one. The helper that turns a failed start into a transcript is Ctx.java.
Going deeper
- Reference: Spring Framework – Using @Autowired
- Source: CandidateResolutionTest.java is the test that writes every transcript in this article
- Previous article: Dependency Injection in Spring Boot 4: Constructor vs Setter vs Field for how the bean arrives once it has been chosen
Two candidates and no hint: the exact error, and what it is telling you
Now give the container twoPaymentGateway beans, StripeGateway and PaypalGateway (both are one-line subclasses of a logging gateway, for example StripeGateway.java), and the same unchanged CheckoutPlain. The scenario is a single line of test code:
var outcome = Ctx.tryStart(StripeGateway.class, PaypalGateway.class, CheckoutPlain.class);
The context does not start. 10-no-unique-bean.txt has the messages:
started: false
top exception : org.springframework.beans.factory.UnsatisfiedDependencyException
top message : Error creating bean with name 'checkoutPlain': Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected single matching bean but found 2: stripeGateway,paypalGateway
root cause : org.springframework.beans.factory.NoUniqueBeanDefinitionException
root message : No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected single matching bean but found 2: stripeGateway,paypalGateway
The fingerprint of the ambiguous case.The container has refused to guess, which is the right behaviour: choosing a payment provider by accident is the kind of mistake that reaches production. Your job is to give it a rule, and there are five rules to choose from. The next section lays them out and shows which wins when several apply at once.expected single matching bean but found 2: stripeGateway,paypalGateway. The two names at the end are the bean names Spring will accept in a@Qualifier, so the message is also the list of valid answers. Stripe was registered first and it did not win: registration order is not a tie-breaker, so “the first one” is never what you get.
Going deeper: why so many start-up failures are this one exception
Interface-with-several-implementations is the normal shape of a codebase that has grown: two payment providers, two notification channels, a real client and a stub. The failure is deliberately loud and deliberately at start-up, because a bean that was silently chosen would only reveal itself as wrong behaviour at run time. The error message includes the candidate names for that reason.
The same message appears when you did not add a second implementation yourself: a library or a Spring Boot auto-configuration may register one. If the count in the message is higher than you expect, list the beans (the dependency graph printed by getDependenciesForBean in 17-dependency-graph.txt is one way) before reaching for an annotation.
Going deeper
- Reference: Fine-tuning annotation-based autowiring with @Primary or @Fallback
- Source: PaypalGateway.java
Five hints, and the order Spring tries them
There are five ways to tell Spring which bean you mean. Three go on the beans (@Primary, @Priority, @Fallback), one goes on the injection point (@Qualifier), and one is implicit (the name).
@Qualifier goes on the injection point and names the bean you want. It is the most explicit, and as row 2 of the table below shows, it overrides a @Primary bean (CheckoutQualified.java):
public class CheckoutQualified {
private final PaymentGateway gateway;
public CheckoutQualified(@Qualifier("paypalGateway") PaymentGateway gateway) {
this.gateway = gateway;
}
public String gatewayClass() {
return gateway.getClass().getSimpleName();
}
}
@Primary goes on one bean and says “when there is a tie, prefer me” (PrimaryStripeGateway.java):
@Primary
public class PrimaryStripeGateway extends StripeGateway {
}
@Priority (from jakarta.annotation) puts a number on a bean; the lowest number wins (PriorityTwoGateway.java):
@Priority(2)
public class PriorityTwoGateway extends PaypalGateway {
}
@Fallback marks a bean that should be used only when nothing else qualifies (FallbackGateway.java). The fifth, implicit hint is the name of the parameter or field, covered in the next section because it has a catch of its own.
@Qualifier narrows the candidate list first; if that leaves one bean, the search is over. Otherwise Spring looks for a @Primary bean, then for a bean whose name equals the parameter or field name, then for the highest @Priority, and finally it prefers a bean that is not a @Fallback. The next table shows what actually happened when each combination was registered.
11-resolution-ladder.txt starts a fresh mini-context per row and prints which class the consumer received:
beans registered / injection point winner
---------------------------------------------------------- --------------------
@Primary Stripe + Paypal; CheckoutPlain PrimaryStripeGateway
@Primary Stripe + Paypal; @Qualifier("paypalGateway") PaypalGateway
@Priority(1) Stripe + @Priority(2) Paypal; CheckoutPlain PriorityOneGateway
@Priority(1) + Paypal(no priority); param named paypalGateway PaypalGateway
Stripe + Paypal (plain); param named paypalGateway PaypalGateway
Stripe (plain) + @Fallback Paypal; CheckoutPlain StripeGateway
@Fast custom qualifier, FastGateway + Paypal; @Fast param FastGateway
Rows 1 and 2 show @Primary deciding a tie and a @Qualifier overriding it. Row 3 shows @Priority deciding a tie between two annotated beans. Row 6 shows @Fallback losing to an ordinary bean even though it was declared alongside it.
The surprise is row 4. One bean carries@Priority(1), the other has no annotation but its name matches the parameter name, and the unannotated bean wins. If you assume “annotations beat names”, this is the row that breaks the assumption. It is measured on Spring Framework 7.0.9 and it agrees with the order of the calls in the bytecode below; do not build a design around a subtle precedence between two hints. If you need a specific bean, say so with@Qualifier.
Going deeper: reading the choice straight out of the framework
The method that makes this decision is DefaultListableBeanFactory.determineAutowireCandidate. Its instruction listing from spring-beans-7.0.9.jar is 23-determine-autowire-candidate-bytecode.txt; the method calls, in order of appearance, are the ladder in the diagram:
# jar: spring-beans-7.0.9.jar
protected java.lang.String determineAutowireCandidate(java.util.Map<java.lang.String, java.lang.Object>, org.springframework.beans.factory.config.DependencyDescriptor);
invokevirtual #1076 // Method org/springframework/beans/factory/config/DependencyDescriptor.getDependencyType:()Ljava/lang/Class;
invokevirtual #1039 // Method determinePrimaryCandidate:(Ljava/util/Map;Ljava/lang/Class;)Ljava/lang/String;
invokevirtual #1169 // Method org/springframework/beans/factory/config/DependencyDescriptor.getDependencyName:()Ljava/lang/String;
invokeinterface #970, 1 // InterfaceMethod java/util/Map.keySet:()Ljava/util/Set;
invokeinterface #397, 1 // InterfaceMethod java/util/Set.iterator:()Ljava/util/Iterator;
invokeinterface #304, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
invokeinterface #309, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
invokevirtual #1393 // Method matchesBeanName:(Ljava/lang/String;Ljava/lang/String;)Z
invokevirtual #150 // Method getAutowireCandidateResolver:()Lorg/springframework/beans/factory/support/AutowireCandidateResolver;
invokeinterface #1172, 2 // InterfaceMethod org/springframework/beans/factory/support/AutowireCandidateResolver.getSuggestedName:(Lorg/springframework/beans/factory/config/DependencyDescriptor;)Ljava/lang/String;
invokeinterface #970, 1 // InterfaceMethod java/util/Map.keySet:()Ljava/util/Set;
invokeinterface #397, 1 // InterfaceMethod java/util/Set.iterator:()Ljava/util/Iterator;
invokeinterface #304, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
invokeinterface #309, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
invokevirtual #1393 // Method matchesBeanName:(Ljava/lang/String;Ljava/lang/String;)Z
invokevirtual #1043 // Method determineHighestPriorityCandidate:(Ljava/util/Map;Ljava/lang/Class;)Ljava/lang/String;
invokevirtual #1046 // Method determineDefaultCandidate:(Ljava/util/Map;)Ljava/lang/String;
invokeinterface #1230, 1 // InterfaceMethod java/util/Map.entrySet:()Ljava/util/Set;
invokeinterface #397, 1 // InterfaceMethod java/util/Set.iterator:()Ljava/util/Iterator;
invokeinterface #304, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
invokeinterface #309, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
invokeinterface #1235, 1 // InterfaceMethod java/util/Map$Entry.getKey:()Ljava/lang/Object;
invokeinterface #1238, 1 // InterfaceMethod java/util/Map$Entry.getValue:()Ljava/lang/Object;
invokeinterface #1399, 2 // InterfaceMethod java/util/Map.containsValue:(Ljava/lang/Object;)Z
Reading it top to bottom: determinePrimaryCandidate first; then a loop that compares each candidate name with getDependencyName via matchesBeanName, and again with the name suggested by the candidate resolver; then determineHighestPriorityCandidate; then determineDefaultCandidate. The loop at the end checks whether a candidate is one of the container’s own resolvable dependencies. The script that produces the listing is capture-bytecode.sh. Bytecode tells you the order of the calls, not the reason for each; the reasons come from the reference documentation linked below.
Going deeper: a custom qualifier instead of a string
A @Qualifier("paypalGateway") repeats a bean name in every place it is used, and a typo is a runtime failure. A custom qualifier turns the string into a type the compiler checks. It is an annotation that is itself annotated @Qualifier (Fast.java):
/** A custom qualifier: a meta-annotation over {@code @Qualifier}, so call sites do not repeat a string. */
@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Fast {
}
Put it on a bean and on the injection point (CheckoutFast.java):
public class CheckoutFast {
private final PaymentGateway gateway;
public CheckoutFast(@Fast PaymentGateway gateway) {
this.gateway = gateway;
}
public String gatewayClass() {
return gateway.getClass().getSimpleName();
}
}
The last row of the table above is that scenario: FastGateway, marked @Fast, is chosen although a second gateway is registered. The reference chapter on qualifiers also covers qualifier attributes and using several qualifiers together.
Going deeper: @Resource chooses by name first
@Resource comes from jakarta.annotation, not from Spring, and it reverses the order of questions: it looks for a bean named after the field first, and falls back to the type. Both forms are in ResourceConsumer.java and the result is in 16-generics-and-resource.txt:
@Resource : paypalGateway field -> PaypalGateway, whateverIWantToCallIt field -> StripeGateway
The plain @Resource on a field called paypalGateway got the PayPal bean; the explicit name = "stripeGateway" on a field with an unrelated name got the Stripe bean. Note the caveat that goes with any name-driven lookup: the field name is a hint that a rename can silently change.
Going deeper
- Reference: Fine-tuning annotation-based autowiring with qualifiers
- Reference: Using @Resource
- Source: FastGateway.java and PriorityOneGateway.java
Matching by parameter name only works if the compiler kept the names
The implicit hint deserves its own section, because it is the one that fails without any annotation to look at. When several beans tie, one of the things Spring compares is the name of the parameter or field with the bean names. A constructor parameter calledpaypalGateway selects the bean called paypalGateway (CheckoutNamed.java):
/** The parameter name is the only hint: it matches the bean name {@code paypalGateway}. */
public class CheckoutNamed {
private final PaymentGateway paypalGateway;
public CheckoutNamed(PaymentGateway paypalGateway) {
this.paypalGateway = paypalGateway;
}
public String gatewayClass() {
return paypalGateway.getClass().getSimpleName();
}
}
There is nothing here that says “use PayPal” except an identifier. That works only if the compiled class file still contains the identifier, and by default javac discards parameter names. To prove it, the test in CandidateResolutionTest.java compiles a class of this shape twice from the same source text, once with -parameters and once without, and loads each into a context that has both gateways:
javac -parameters : started, picked PaypalGateway
javac (no flag) : FAILED NoUniqueBeanDefinitionException
No qualifying bean of type 'com.ankurm.coredi.injection.PaymentGateway' available: expected single matching bean but found 2: stripeGateway,paypalGateway
The fingerprint.Spring Boot projects rarely meet this because the Boot parent pom turns the flag on. 26-boot-parent-parameters-flag.txt is the relevant lines fromexpected single matching bean but found 2for a class that looks as if it names its bean. If a parameter or field name is the only thing selecting a bean, and a build that used to work starts failing after a build-tool or compiler-option change, check the-parametersflag first.
spring-boot-starter-parent-4.1.1.pom:
111- <artifactId>maven-compiler-plugin</artifactId>
112- <configuration>
113: <parameters>true</parameters>
114- </configuration>
115- </plugin>
Going deeper: whether to rely on it at all
The name fallback is convenient and fragile in equal measure. It disappears when the flag does, it changes when someone renames a field for unrelated reasons, and it is invisible to readers. It is reasonable for a throwaway class in a small application where the two beans are obviously distinguished by name; for anything a colleague will maintain, a @Qualifier or a custom qualifier costs one line and survives a rename. The measurement above is the whole of what was verified here: on 7.0.9, without the flag, the fallback did not happen.
The flag matters beyond this one feature — anything in Spring that reads parameter names from the class file depends on it — and the Boot parent pom’s setting is why most Boot applications never think about it. If your build does not inherit from spring-boot-starter-parent, you own this setting.
Going deeper
- Source: CheckoutNamed.java and the test method
parameterNameFallbackNeedsTheParametersFlagin CandidateResolutionTest.java - Related on this site: Spring Framework 6 to 7 Migration Guide for other things that changed with the 7.0 line
When the bean may not exist: Optional, @Nullable and ObjectProvider
Sometimes a dependency is genuinely optional: an audit sink that exists only in some environments, a metrics client that a test profile leaves out. Spring gives you four ways to say “this may be absent” (the plain parameter is in the transcript for comparison), and they agree while the bean is missing and disagree as soon as there are two of them. The class that holds them all is OptionalConsumers.java; the provider variant is representative:
public static class WithProvider {
public final ObjectProvider<Notifier> provider;
public WithProvider(ObjectProvider<Notifier> provider) {
this.provider = provider;
}
}
With zero Notifier beans, 13-optional-and-objectprovider.txt shows:
--- zero Notifier beans ---
plain constructor param : FAILED NoSuchBeanDefinitionException
Optional<Notifier> : isPresent=false
@Nullable Notifier : value=null
ObjectProvider.getIfAvailable() : null
ObjectProvider.getIfUnique() : null
ObjectProvider.getObject() : NoSuchBeanDefinitionException: No qualifying bean of type 'com.ankurm.coredi.injection.Notifier' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
@Autowired(required=false) setter: value=null
A plain parameter fails, as it should. Optional is empty, @Nullable receives null, an ObjectProvider answers null from getIfAvailable() and getIfUnique() but throws from getObject(), and the setter marked required = false is simply not called. Now register two notifiers, SmsNotifier (@Order(1)) and EmailNotifier (@Order(2)), with Email registered first:
--- two Notifier beans (Sms, Email), no @Primary ---
ObjectProvider.getIfAvailable() : NoUniqueBeanDefinitionException: No qualifying bean of type 'com.ankurm.coredi.injection.Notifier' available: expected single matching bean but found 2: emailNotifier,smsNotifier
ObjectProvider.getIfUnique() : null
ObjectProvider.stream() classes : [EmailNotifier, SmsNotifier]
ObjectProvider.orderedStream() : [SmsNotifier, EmailNotifier]
Optional<Notifier> with two beans: FAILED NoUniqueBeanDefinitionException
No qualifying bean of type 'com.ankurm.coredi.injection.Notifier' available: expected single matching bean but found 2: emailNotifier,smsNotifier
Optionalis not a tie-breaker.Optional<Notifier>reads as “one or nothing”, but two beans is neither, and the injection fails with the sameNoUniqueBeanDefinitionExceptionas a plain parameter.ObjectProvider.getIfAvailable()throws the same exception;getIfUnique()is the method that returnsnullfor “not exactly one”. Choose the method by what you want when the count is not one.
stream() and orderedStream() are how you take all of them: the first followed registration order (Email, Sms) and the second followed @Order (Sms, Email).
Going deeper: what an ObjectProvider actually is
An ObjectProvider<T> is a small handle to the container’s lookup for T. The dependency is not resolved when your class is built; it is resolved each time you call a method on the handle. That is why it can express “maybe none”, “maybe several” and “a fresh one each time” — the last of which is how it repairs a prototype bean injected into a singleton, demonstrated in the article on bean scopes.
The nullability annotation used in the repository’s @Nullable variant is org.jspecify.annotations.Nullable; Spring’s own org.springframework.lang.Nullable is deprecated since 7.0, as the previous article verified from the class file.
Going deeper
- Reference: Spring Framework – Using @Autowired (the sections on
required,Optional,@NullableandObjectProvider) - Source: EmailNotifier.java and SmsNotifier.java
Injecting every bean of a type: List, Map and Set
The cleanest answer to “which of my three implementations?” is often “all of them”. Ask for a collection and Spring injects every bean of the element type, which is how a set of notification channels, validators or plug-ins gets wired without an if chain (Broadcaster.java):
/** Receives every Notifier three ways: as a List, a Map keyed by bean name, and a Set. */
public class Broadcaster {
public final List<Notifier> asList;
public final Map<String, Notifier> asMap;
public final Set<Notifier> asSet;
public Broadcaster(List<Notifier> asList, Map<String, Notifier> asMap, Set<Notifier> asSet) {
this.asList = asList;
this.asMap = asMap;
this.asSet = asSet;
}
}
Three notifiers were registered in the order Push, Email, Sms, and 14-list-map-set-injection.txt shows what each shape received:
List<Notifier> order : [SmsNotifier, EmailNotifier, PushNotifier]
Map<String,Notifier> : [pushNotifier, emailNotifier, smsNotifier]
Set<Notifier> size : 3
List is sorted: @Order(1) on SmsNotifier puts it first, @Order(2) puts Email second, and PushNotifier, which has no annotation, comes last even though it was registered first. The Map is keyed by bean name, which makes it the natural shape for “pick the strategy by a string from a request”. In this run its keys came out in registration order; only that one registration order was measured, so do not read more into it, and put the order you need in @Order on beans you take as a List.
The order is worth caring about when the collection is a chain: authorisation checks that must run before logging, or a list of fallbacks tried in turn. Without @Order, the List order is whatever the container happens to produce, which is not something to build a chain on.
Going deeper: the generic type is part of the request
An injection point List<Handler<String>> collects only the beans that are Handler<String>, and a parameter Handler<Integer> picks the Handler<Integer> bean without any qualifier, although both implement the same interface (GenericConsumer.java):
/** The generic type argument is part of the injection point: only Handler<String> beans arrive. */
public class GenericConsumer {
public final List<Handler<String>> stringHandlers;
public final Handler<Integer> integerHandler;
public GenericConsumer(List<Handler<String>> stringHandlers, Handler<Integer> integerHandler) {
this.stringHandlers = stringHandlers;
this.integerHandler = integerHandler;
}
}
The result, from 16-generics-and-resource.txt:
List<Handler<String>> : [StringHandler]
Handler<Integer> : IntegerHandler
So generics are a fourth kind of narrowing that costs nothing to write: if your two implementations differ in a type argument, the tie never happens.
Going deeper
- Reference: Spring Framework – Using @Autowired (injecting collections and maps)
- Source: PushNotifier.java (the bean with no
@Order)
The empty-collection trap: constructors say yes, fields and setters say no
One more case sits between “required” and “optional”: a collection of beans when zero beans of that type exist. A plug-in host with no plug-ins installed is a perfectly reasonable state. Whether it starts depends on which injection style you chose, and the four hosts in PluginHosts.java were written to show it. The constructor form and the field form differ by a few lines: public static class Plain {
public Plain(List<Plugin> plugins) {
}
}
public static class FieldInjected {
@Autowired
public List<Plugin> plugins;
}
There are no Plugin beans in any of these runs. 15-empty-collection.txt shows the outcome:
--- required List<Plugin>, zero Plugin beans ---
single constructor param : started (empty list injected)
@Autowired ctor + no-arg ctor : started (empty list injected)
FieldInjected : FAILED NoSuchBeanDefinitionException
No qualifying bean of type 'java.util.List<com.ankurm.coredi.resolution.Plugin>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
SetterInjected : FAILED NoSuchBeanDefinitionException
No qualifying bean of type 'java.util.List<com.ankurm.coredi.resolution.Plugin>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
The constructor versions start — Spring injects an empty list — including a class that has two constructors with the one taking the list marked @Autowired. The field and the setter versions fail with a NoSuchBeanDefinitionException, although the injected type is the same. If you want an absent collection to be legal at every injection point, the second half of the transcript shows four ways:
--- ways to make an absent collection legal ---
Optional<List<Plugin>> : isPresent=false
@Nullable List<Plugin> : value=null
ObjectProvider<Plugin> -> list : []
@Autowired(required=false) list: []
The fingerprint.No qualifying bean of type 'java.util.List<com.ankurm.coredi.resolution.Plugin>'— a collection type inside the quotes — and, on the field variant,Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}showing thatrequired=trueis the default. A refactor from a constructor to a field can turn “starts with no plug-ins” into “will not start”, and nothing in the diff looks like a behaviour change.
Going deeper: what to do about it
The practical rule is to say what you mean. If “no plug-ins” is legal, spell it: @Autowired(required = false), an Optional<List<Plugin>>, a @Nullable list, or an ObjectProvider and orderedStream(). The last is the version that also tolerates two or more beans and keeps them in @Order, which is why it is the one used in WithProvider. Do not depend on the constructor leniency: it is a fallback for one style, not a contract for the design, and the previous article shows how easily the rules for choosing a constructor change when a second one is added.
Going deeper
- Source: CandidateResolutionTest.java (
emptyCollections) - Reference: Spring Framework – Using @Autowired
So which hint, and when
The table collects the whole article as a set of situations. Read it as “what do I want the code to say”.| You want | Write | Why |
|---|---|---|
| exactly this bean, always | @Qualifier or a custom qualifier at the injection point | explicit, survives renames when the qualifier is a type |
| a sensible default with exceptions | @Primary on the default bean | one annotation, and a @Qualifier still overrides it |
| a bean only when nothing else exists | @Fallback | the bean steps aside for any ordinary candidate |
| all of them | List<T> or Map<String, T> | no tie to break; order with @Order |
| it may not exist | ObjectProvider<T> or an optional setter | absence is written in the type; pick the method for the count you want |
| a collection that may be empty | say so explicitly | field and setter injection do not tolerate an empty collection |
| to rely on a parameter name | avoid it | it needs -parameters and is invisible to readers |
Should you even care, on a small project? If every interface in your application has one implementation, none of this will ever fire, and you can leave it unread. It starts to matter the day a second implementation appears — often by adding a stub, a second provider or a library that registers its own bean — and the message at that point is a start-up failure, which is the cheapest place to learn it. Two points are opinion rather than measurement: prefer an explicit@Qualifierat the injection point over@Primarywhen a wrong choice would be costly, because it reads as a decision at the call site; and treat a growing pile of hints as a sign that the abstraction is doing two jobs.
Going deeper: hints in the reference documentation
The reference chapter on @Autowired (Using @Autowired) covers the annotation itself, its use on constructors, setters and fields, and collections; the pages on @Primary and @Fallback and qualifiers cover the two hints in detail. Everything they document in prose has a runnable counterpart in this article’s transcripts, which is the reason to trust the parts where this article says “measured” over anything remembered from an older Spring version.
Going deeper
- Previous: Dependency Injection in Spring Boot 4: Constructor vs Setter vs Field
- Next: Spring Bean Scopes and the Prototype-in-Singleton Trap (where
ObjectProviderrepairs a real bug) and Spring Bean Lifecycle in Boot 4 - Related on this site: @ConfigurationProperties vs @Value in Spring Boot 4
Further reading
- Companion repository for this article: asmhatre/spring-boot-demo, core-di module
- Other articles in this series: Constructor vs Setter vs Field injection, Spring Bean Scopes, Spring Bean Lifecycle
- Interview preparation: Top 50 Spring Boot 4 Interview Questions and Answers (2026)
- Official reference: Spring Framework – Using @Autowired
- Official reference: Spring Framework – Fine-tuning with qualifiers and @Primary and @Fallback
- Official reference: Spring Framework – Using @Resource
No Comments yet!