skip to content

Walk through the resolution order Spring uses for a single-valued @Autowired point, and when NoSuchBeanDefinitionException vs NoUniqueBeanDefinitionException is thrown.

level: seniorimportance: must knowfreq 70%

answer

  1. collect assignable candidates
  2. 0 + required -> NoSuchBean
  3. N -> @Primary > @Priority > name match
  4. still ambiguous -> NoUniqueBean
  5. collections take all, never throw

basics

~20 s

Spring first collects all beans assignable to the required type. Zero candidates and required -> NoSuchBeanDefinitionException. One candidate -> inject it. Many candidates -> narrow by @Primary, then @Qualifier, then by matching the field/parameter name; if still not unique -> NoUniqueBeanDefinitionException.

solid answer

~40 s

For a single-valued injection point, DefaultListableBeanFactory.doResolveDependency runs: first it checks for a @Value/suggested value (short-circuits bean lookup); otherwise it gathers candidate beans assignable to the required type via findAutowireCandidates. If the map is empty and the dependency is required, it throws NoSuchBeanDefinitionException. If exactly one candidate, that is injected. If several, it calls determineAutowireCandidate to break the tie: a @Primary bean wins; failing that, the highest @Priority; failing that, a candidate whose bean name equals the field or parameter name acts as an implicit qualifier. (@Qualifier is applied earlier to filter the candidate set — that mechanism belongs to the qualifier leaf.) If no unique winner emerges, it throws NoUniqueBeanDefinitionException listing the competing bean names. Collections and Map injection instead take all candidates and never throw on multiplicity.

code

java · 24 lines
java
interface Codec {}
@Component class JsonCodec implements Codec {}
@Component class XmlCodec  implements Codec {}

@Component
class Encoder {
    // Two Codec candidates, no @Primary/@Qualifier.
    // Parameter name is 'codec' — matches NEITHER bean name (jsonCodec/xmlCodec),
    // so no unique winner -> NoUniqueBeanDefinitionException at startup.
    Encoder(Codec codec) { }
}

@Component
class Encoder2 {
    // Parameter name 'jsonCodec' matches the jsonCodec bean name ->
    // implicit name tie-break selects it. Wiring succeeds.
    Encoder2(Codec jsonCodec) { }
}

@Component
class Multi {
    // Multi-valued: injects BOTH; empty list if none, never throws.
    Multi(List<Codec> allCodecs) { }
}

go deeper

for a junior

Know zero = fails, one = works, many = ambiguous.

for a middle

Name both exceptions and that a name match can break a tie.

for a senior

Order the tie-break steps (@Primary -> @Priority -> name) and contrast scalar vs collection injection.

for a principal

Trace doResolveDependency/determineAutowireCandidate and the exception subclass relationship, and reason about candidate eligibility and generics filtering.

### The resolution pipeline (single-valued point) Driven by `DefaultListableBeanFactory.doResolveDependency` / `resolveDependency`: 1. **Suggested value / @Value short-circuit** — `getAutowireCandidateResolver().getSuggestedValue(descriptor)` is checked first. If the point has `@Value`, the resolved value is used and **no bean lookup happens**. 2. **Shortcut / lazy proxy** checks (e.g. `@Lazy` produces a proxy). 3. **Find candidates** — `findAutowireCandidates(...)` returns a `Map<String beanName, Object beanOrType>` of every bean **assignable** to the required type that is also an eligible **autowire candidate** (a bean can opt out via `autowire-candidate=false` / `defaultCandidate=false`). 4. **Zero candidates**: - If the dependency is **required** (default), throw **`NoSuchBeanDefinitionException`** — context fails to start. - If **optional** (`required=false`, `Optional<T>`, `@Nullable`), inject null/empty. 5. **Exactly one candidate** — inject it. 6. **Multiple candidates** — call `determineAutowireCandidate` to elect a single winner, in order: 1. **`@Primary`** — the bean marked primary among the candidates wins. Two primaries → still ambiguous. 2. **`@Priority(n)`** (`jakarta.annotation.Priority`) — **lowest** number = highest priority wins. 3. **Fallback by name** — a candidate whose **bean name matches the field/parameter name** is chosen (the parameter name acts as an implicit qualifier). - If none of these yields a unique bean → throw **`NoUniqueBeanDefinitionException`**, whose message lists the competing candidate names. > Note: **`@Qualifier`** narrows the candidate *set* in step 3 (a filter), and `@Primary`/`@Fallback` tie-breaking is the dedicated qualifier-primary leaf's territory — here the point is *where in the order* the two exceptions arise. ### The two exceptions contrasted | Situation | Exception | |---|---| | No bean of the type (and required) | `NoSuchBeanDefinitionException` | | More than one bean and no unique winner | `NoUniqueBeanDefinitionException` (a subclass of `NoSuchBeanDefinitionException`) | Because `NoUniqueBeanDefinitionException extends NoSuchBeanDefinitionException`, catching the latter catches both — but the messages differ ("no qualifying bean" vs "expected single matching bean but found N"). ### Multi-valued points are different `@Autowired List<T>`, `T[]`, `Set<T>`, `Map<String,T>`, `Stream<T>`, `ObjectProvider<T>` are **not** single-valued: Spring injects **all** candidates. An empty collection is injected when none match — **no exception** even for a "required" collection. For `Map`, keys are bean names. Ordering follows `@Order`/`Ordered`. ### Common gotchas - A required scalar with **two** candidates and **no** `@Primary`/`@Qualifier`/name match → `NoUniqueBeanDefinitionException`, a classic startup failure. - Renaming a field so it no longer matches any bean name removes the implicit-name tie-break and can suddenly make wiring ambiguous. - `required=false` does **not** rescue ambiguity — it only handles the zero-candidate case. - Generic type parameters filter candidates (`Converter<String,Integer>` won't match `Converter<Integer,String>`). ### When to reason about this Debugging startup failures, understanding why adding a second implementation broke wiring, or explaining implicit name-based resolution.

  • Two beans of the type exist, neither @Primary, and the parameter name matches one bean name. What is injected?
    The bean whose name equals the parameter name. After @Primary/@Priority fail to elect a winner, Spring falls back to name matching where the field/parameter name acts as an implicit qualifier, so that candidate wins and no exception is thrown.
  • How are the two exceptions related?
    NoUniqueBeanDefinitionException is a subclass of NoSuchBeanDefinitionException. Catching NoSuchBeanDefinitionException catches both, but 'no qualifying bean' means zero candidates while 'expected single matching bean but found N' means ambiguity.

saying these in an interview costs you the question

  • Claiming multiple candidates throw NoSuchBeanDefinitionException (it's NoUniqueBeanDefinitionException)
  • Saying @Autowired List throws when several beans match
  • Believing required=false resolves ambiguity
  • Forgetting the field/parameter name acts as an implicit tie-breaker

context