Home › Java › Could Not Autowire in Spring? Fix It Fast
Intermediate 6 min · September 23, 2026

Could Not Autowire in Spring? Fix It Fast

Fix Spring's Could not autowire with @Qualifier or @Primary for ambiguity, scan-layout fixes for gaps, and cycle-breaking for loops..

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
✓ Production
production tested
September 25, 2026
last updated
1,950
articles · all by Naren
Before you start⏱ 14 min
  • ✓Basic Java and Spring Boot project knowledge
  • ✓Comfort reading stack traces in a terminal
  • ✓A Spring Boot 3 project you can experiment with
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Could not autowire has two families: no qualifying bean (zero candidates, usually a scan gap) and multiple candidates (ambiguity needing a tiebreak)
  • Read the nested Caused by chain first — it names the exact type, the injection point, and which family you're in
  • Fix ambiguity with @Qualifier at the injection point or @Primary for the default, never both at once
  • Fix missing beans by checking stereotypes and scan roots; fix constructor cycles by breaking the loop, not by switching to field injection
✦ Definition~90s read
What is Spring Could Not Autowire Fix?

Could not autowire is Spring's way of saying dependency injection failed while building the application context. Your class declares a need — a constructor parameter, a field, a @Bean method argument — and the container can't satisfy it from the beans it knows.

★
Think of Spring as a restaurant kitchen with a rule: every dish lists its ingredients, and the pantry must hold exactly one matching item per line.

Startup aborts with UnsatisfiedDependencyException naming the injection point, wrapping a nested cause that carries the real diagnosis. Nothing serves traffic until the context builds, so every variant here is a full outage for that deploy.

The container reasons in three steps. First it collects candidate beans assignable to the required type from scanned stereotypes, @Bean methods, and auto-configurations. Then it filters by qualifiers and conditions. Finally it demands exactly one survivor: zero means NoSuchBeanDefinitionException (nothing registered or visible), two or more without a tiebreak means NoUniqueBeanDefinitionException (ambiguity).

A third failure, BeanCurrentlyInCreation, means the survivor set contains a construction loop. Each cause has its own fix, which is why reading the nested chain matters more than any annotation you could guess.

Registration and visibility decide the candidate set before selection even runs. Stereotypes (@Component, @Service, @Repository, @Controller) register classes found by component scanning, which starts at your @SpringBootApplication package and descends. @Bean methods register explicit instances for classes you can't annotate.

Conditionals can silently remove candidates per environment — the classic staging-green-production-red split. Selection then picks with @Qualifier (this point wants that bean) and @Primary (the default). Constructor injection makes the whole process fail fast; field injection defers the pain to stranger failures later.

Plain-English First

Think of Spring as a restaurant kitchen with a rule: every dish lists its ingredients, and the pantry must hold exactly one matching item per line. Could not autowire means the pantry check failed — either the shelf is empty (no qualifying bean) or two jars share a label (multiple candidates). @Qualifier writes a brand name on the order ticket. @Primary marks one jar as the house default. And if the pantry room was never inventoried because it's on another floor, that's a component-scan gap.

You add one annotation, hit run, and Spring refuses to start: Could not autowire. No qualifying bean of type com.acme.BillingService. The class exists. The annotation is right there. Yet the application context won't build, the pipeline is red, and the error dumps a stack trace that reads like a riddle. Every Spring developer meets this failure, usually right before a demo.

The instinct is to annotate harder — add @Qualifier, add @ComponentScan, mark everything @Primary — until startup passes. That scattergun approach works once and poisons the codebase: qualifiers that name beans which don't exist, scan roots that sweep up test doubles, primaries that silently pick the wrong implementation for months. Each guess adds a line that the next developer must decode.

This guide teaches you to read the nested exception like a diagnosis instead of a complaint. You'll learn the two failure families — zero candidates versus too many — and the exact fix for each: @Qualifier and @Primary for ambiguity, scan layout for missing beans, cycle-breaking for constructor loops. By the end you'll fix wiring errors in minutes with one deliberate change instead of five hopeful ones.

No Qualifying Bean vs Multiple Candidates: Read the Error First

Every Could not autowire failure belongs to one of two families, and the nested exception tells you which. No qualifying bean of type X means zero candidates: Spring scanned everything and found nothing assignable to X. No unique bean of type X means two or more candidates with no tiebreak: Spring found options and refuses to guess. The outer UnsatisfiedDependencyException just says which injection point failed; the inner Caused by carries the diagnosis. Read inside-out, deepest cause first.

Zero-candidate failures trace to registration or visibility. The class lacks a stereotype (@Component, @Service, @Repository, @Controller), the @Bean method sits in a configuration class that's never scanned, or a conditional like @ConditionalOnProperty evaluated false. Confirm by dumping the beans Spring knows: hit /actuator/beans in a running sibling environment or add a failing context test that prints applicationContext.getBeanDefinitionNames(). If your type is absent from that list, no annotation at the injection point can help — you must register the bean.

Multi-candidate failures trace to growth: a second implementation merged, a test configuration leaked into the main scan, or a library auto-configuration contributed a bean you didn't expect. The exception message helpfully lists every match by name — read them. Each listed name is a real bean from a real source, and your fix is choosing among them once, explicitly. Never let the message's length panic you into @Primary-everything; two named candidates need one decision, not three annotations.

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
// Injection point from the incident — constructor states its need
@Service
public class CheckoutService {
    private final BillingService billing;

    public CheckoutService(BillingService billing) {
        this.billing = billing;
    }
}
// Zero beans  -> NoSuchBeanDefinitionException: No qualifying bean
// Two beans   -> NoUniqueBeanDefinitionException: expected single bean
// Read the nested 'Caused by' — it names the type and the family.
📊 Production Insight
A team spent a morning adding qualifiers to a bean that didn't exist. The actuator beans list showed the truth in seconds: a @ConditionalOnProperty had evaluated false in production. Registration first, selection second.
🎯 Key Takeaway
Zero candidates means register or reveal the bean; several means pick one explicitly. The nested cause names your family.

Fixing Ambiguity with @Qualifier at the Injection Point

@Qualifier resolves ambiguity at a single injection point by naming the bean you want. It pairs with explicit bean names — @Service("stripeBilling") — so the choice reads clearly at both ends. Constructor parameters take @Qualifier directly, which keeps the selection next to the dependency it governs. When checkout needs Stripe and refunds need PayPal, each constructor states its own answer and neither cares about the other.

Name beans deliberately or Spring names them for you with the decapitalized class name (stripeBillingService). Explicit names survive refactors of class names and read better in error messages. A useful convention: name by provider or role — stripeBilling, paypalBilling — not by layer. When the exception lists candidates, those names should already tell a reviewer which one each caller wants.

Scope @Qualifier to genuine per-caller choices. If every injection point names the same bean, that's not selection — that's a missing default, and @Primary expresses it in one place instead of ten. Qualifiers scattered identically across a codebase rot fast: rename the bean and you hunt ten files. One qualifier per genuinely different choice; a primary for the common case. Reviewers should be able to ask of each qualifier, why is this caller special, and get a real answer.

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Name the implementation once, choose it at each injection point
@Service("stripeBilling")
public class StripeBillingService implements BillingService { }

@Service("paypalBilling")
public class PaypalBillingService implements BillingService { }

@Service
public class CheckoutService {
    private final BillingService billing;

    public CheckoutService(@Qualifier("stripeBilling") BillingService billing) {
        this.billing = billing;
    }
}
📊 Production Insight
A checkout service qualified stripeBilling while refunds qualified paypalBilling. When a third provider arrived, only new callers needed decisions — existing ones kept working untouched.
🎯 Key Takeaway
Use @Qualifier for per-caller choices with explicit bean names. If every caller agrees, that's a @Primary instead.

Setting the Default with @Primary — and Its Blast Radius

@Primary declares the default bean for a type across the whole context. Plain injection points resolve to it without further annotation, while exceptional callers override with @Qualifier. It shines when one implementation is obviously the common case — the live payment provider versus the sandbox stub, the real clock versus the fixed test clock. One annotation settles every undecided injection point at once.

That global power is exactly why @Primary demands restraint. Two primaries of the same type fail fast, but one wrong primary fails silently: the context loads, health checks pass, and the wrong implementation serves traffic. The billing incident in this article is that story — a guessed @Primary wired the wrong provider while every dashboard stayed green. Mark a bean primary only when you'd bet the business on it being the default, and say so in a comment naming the exceptions.

Choose between the two with a simple rule. Caller-specific choice goes to @Qualifier; codebase-wide default goes to @Primary; both together means the qualifier wins at its own point. Never stack them as guesses during debugging — apply one, re-run, and assert the wired type in a context test. A test like assertThat(context.getBean(BillingService.class)).isInstanceOf(StripeBillingService.class) turns silent miswiring into a build failure.

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// One default for the whole context — use sparingly
@Service
@Primary
public class StripeBillingService implements BillingService { }

// Every plain injection now resolves to Stripe;
// exceptional callers still override with @Qualifier("paypalBilling")
@Service
public class CheckoutService {
    private final BillingService billing;

    public CheckoutService(BillingService billing) {
        this.billing = billing;
    }
}
📊 Production Insight
A guessed @Primary wired sandbox billing in staging and passed all health checks. A bean-type assertion added afterward now fails the build in four seconds when the default drifts.
🎯 Key Takeaway
@Primary sets the global default; @Qualifier overrides per caller. Assert the wired type in tests so a wrong default fails the build.

Component-Scan Gaps: The Bean Exists but Spring Never Looks

Component scanning starts at the package of your @SpringBootApplication class and covers everything below it. Classes in sibling or parent packages are invisible no matter how many stereotypes they carry. This bites during refactors that move the app class into a submodule, multi-module builds where a library package sits outside the root, and copy-pasted starters whose package doesn't match yours. The symptom is maddening: the annotation is right there, the code compiles, and Spring insists the bean doesn't exist.

Diagnose with packages, not annotations. Compare the app class package against the implementation's package with two find commands; if the implementation isn't under the root, you've found it. The actuator beans endpoint confirms from the runtime side — grep it for your class and expect no hit. Resist the urge to scatter @ComponentScan across configuration classes; competing scan roots interact badly and sweep up test doubles. One root, declared once, at the top of your package tree.

When code genuinely lives elsewhere — a shared company library — widen deliberately with scanBasePackages and review that line like API surface, because it is. Document why the external package is included and what it contributes. Better still, have the library ship an auto-configuration so consumers get beans without widening scans at all. The rule that prevents recurrence: the app class stays at the root, and every scan widening carries a comment with its reason.

JAVA
1
2
3
4
5
6
7
8
9
// com.acme.App sits above everything it should wire
package com.acme;

@SpringBootApplication // scans com.acme.** by default
public class App { }

// Explicit widening when code must live elsewhere — review this line
@SpringBootApplication(scanBasePackages = {"com.acme", "com.shared.lib"})
public class WidenedApp { }
⚠ The App Class Package Is the Scan Root
Moving the @SpringBootApplication class down the package tree silently unregisters beans. Keep it at the root and treat any scanBasePackages addition as a reviewed, deliberate widening.
📊 Production Insight
A refactor moved the app class into com.acme.web and orphaned thirty beans overnight. The fix was moving one file back up — but only after an hour of qualifier guessing that changed nothing.
🎯 Key Takeaway
Scanning covers the app package and below. Keep the app class at the root; widen scans only deliberately and documented.

Circular Dependencies: Why Constructor Cycles Fail So Loudly

Constructor cycles — A needs B needs A — fail with BeanCurrentlyInCreation, and the nested chain prints the loop for you. Constructors are honest: they demand the dependency before the object exists, so a loop is unbuildable by construction. That's a feature, not a limitation. The error is telling you the design has a knot, and papering over it with field injection just moves the knot somewhere darker.

Break the loop by removing one edge, not by hiding it. The cleanest cut is usually events: A publishes OrderPlaced and B listens, so A no longer depends on B at all. Alternatives fit other shapes — ObjectProvider<B> defers one lookup until first use, @Lazy on one parameter builds a proxy, and splitting shared logic into a third service C that both depend on dissolves the knot entirely. Pick the option that matches the domain: events for notifications, providers for genuinely optional collaborators, extraction for shared logic both sides wanted.

What you must not do is switch to field injection to make the error vanish. Field-injected cycles start up and then fail unpredictably — nulls in some paths, half-wired proxies in others — because the container can no longer enforce build order. Spring Boot 2.6+ bans circular references by default precisely to force the honest failure. Keep constructors, read the loop from the exception, and cut one edge for real.

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Break the loop: one edge becomes an event
@Service
public class OrderService {
    private final ApplicationEventPublisher events;

    public OrderService(ApplicationEventPublisher events) {
        this.events = events;
    }

    public void place(Order o) {
        events.publishEvent(new OrderPlaced(o)); // AuditService listens
    }
}

@Service
public class AuditService {
    private final OrderService orders; // one-directional now

    public AuditService(OrderService orders) {
        this.orders = orders;
    }
}
📊 Production Insight
An orders-audit cycle was cut by publishing OrderPlaced events instead of calling back directly. Startup passed, and the audit trail gained replayability the team hadn't planned on.
🎯 Key Takeaway
Constructor cycles fail fast by design. Cut one edge with events, providers, or extraction — never with field injection.

Field vs Constructor Injection: End the Debate for Good

Constructor injection lists every dependency in the signature, marks fields final, and works with plain new in unit tests. The compiler enforces completeness: you cannot build the object without its collaborators. New team members read the constructor and know what the class needs; reviewers see added dependencies as signature changes, not hidden field edits. For wiring errors specifically, constructors fail at startup with the exact missing type instead of null-pointering at 2 AM.

Field injection — @Autowired on a private field — hides all of that. Dependencies are invisible to the compiler, untestable without reflection or a container, and mutable by anyone with a setter or a test utility. It also masks cycles: two field-injected services start fine and then NPE in whichever method runs first. The startup error you avoided was the cheap one; the production null is the expensive one. Lombok's @RequiredArgsConstructor removes the verbosity objection entirely — you write the final fields and get the constructor free.

Standardize on one style and enforce it. Ban field and setter @Autowired with a review rule or an ArchUnit test, require final fields, and let constructor injection be the only path. Mixed codebases pay a steady tax: every wiring failure takes longer because the reader must check three injection styles per class. One style means the diagnosis in this article always starts at the constructor, where the answer lives.

JAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Constructor injection: explicit, final, testable without Spring
@Service
public class CheckoutService {
    private final BillingService billing;
    private final OrderRepository orders;

    public CheckoutService(BillingService billing, OrderRepository orders) {
        this.billing = billing;
        this.orders = orders;
    }
}

// Plain unit test — no container, no reflection
@Test
void chargesOnPlace() {
    var svc = new CheckoutService(new FakeBilling(), new FakeOrders());
    assertDoesNotThrow(() -> svc.place(order()));
}
📊 Production Insight
After banning field injection, one team's wiring failures all started failing at startup with exact type names. Mean time to fix dropped from an hour to under ten minutes.
🎯 Key Takeaway
Constructors make dependencies explicit, final, and testable. Ban field injection and enforce the rule automatically.
● Production incidentPOST-MORTEMseverity: high

Two Billing Beans, Zero Tiebreak: Friday Merge Blocked Monday Deploy for 22 Min

Symptom
Monday's production deploy failed at startup with NoUniqueBeanDefinitionException: expected single matching bean but found 2: stripeBillingService, paypalBillingService. Kubernetes killed the pods in a crash loop for 22 minutes while checkout traffic routed to the remaining old pods at reduced capacity. Staging had been green all weekend, which made the team distrust the error instead of reading it.
Assumption
The team assumed the new provider class lacked an annotation and added @Service to it, then @Qualifier at the injection point, then @Primary for good measure. Each change altered the error without fixing it, and the third change masked the real problem: with @Primary present, startup passed in staging but wired the wrong provider. Nobody noticed because the smoke test only checked HTTP 200, not which provider processed the charge.
Root cause
A new PayPalBillingService implementing BillingService merged without a @Qualifier update at the checkout injection point. Staging started because a developer profile excluded the new class, so only Stripe's bean existed there. Production loaded both beans, and constructor injection correctly refused to guess, throwing NoUniqueBeanDefinitionException. The stacked @Primary guess then made staging wire PayPal silently — caught only because a finance reconciliation flagged test charges before real traffic arrived.
Fix
All three annotations were reverted except one @Qualifier("stripeBilling") at the checkout injection point, and the new provider was registered under its own explicit name. The team added a context test asserting the wired bean's class, plus a startup log line printing the active billing provider. Total change was four lines; the review rule going forward is one wiring fix per failure, verified by a bean-type assertion.
Key lesson
  • Stacking @Qualifier, @Primary, and scan changes in one commit hides which fix worked and can wire the wrong bean silently. Change one thing, re-run, and assert the bean type.
  • Smoke tests must verify behavior, not just startup. A context that loads with the wrong primary passes every health check while misrouting real work.
  • Ambiguity errors are design feedback: two live implementations of a billing interface deserve explicit names and an explicit choice at each injection point.
Production debug guideFive patterns cover nearly every instance of this error — identify yours with these exact commands before changing anything.5 entries
Symptom · 01
Startup fails naming one missing type with NoSuchBeanDefinitionException
→
Fix
Read the deepest Caused by, which names the missing type and the injection point: run ./mvnw spring-boot:run 2>&1 | grep -A 6 'Caused by' | head -40. If it says NoSuchBeanDefinitionException for com.acme.BillingService, check registration: grep -rn '@Service\|@Component\|@Bean' src/main/java/com/acme | grep -i billing. No hit means the class was never registered.
Symptom · 02
Startup fails listing two or more beans with NoUniqueBeanDefinitionException
→
Fix
List every candidate Spring sees: run ./mvnw spring-boot:run 2>&1 | grep -B 2 -A 12 'NoUniqueBeanDefinitionException' to get both bean names. Then decide once: annotate the default with @Primary, or add @Qualifier("stripeBilling") at the injection point. Re-run and confirm the context loads — don't add both fixes.
Symptom · 03
Class has a stereotype annotation but Spring still reports no bean
→
Fix
Compare packages: run head -20 src/main/java/com/acme/App.java to find the app class package, then find src/main/java -name 'BillingService.java'. If the implementation sits outside com.acme.*, move it under the root or set @SpringBootApplication(scanBasePackages = "com.acme"). Verify with curl localhost:8080/actuator/beans | grep -i billing.
Symptom · 04
Startup fails with BeanCurrentlyInCreation naming beans in a loop
→
Fix
Print the cycle from the nested chain: run ./mvnw spring-boot:run 2>&1 | grep -E 'Caused by|currently in creation' | head -20. The loop reads A needs B needs A. Break it by replacing one constructor edge with ApplicationEventPublisher, ObjectProvider<B>, or a @Lazy parameter. Re-run until BeanCurrentlyInCreation disappears.
Symptom · 05
Tests pass with Spring but fail as plain unit tests, or fields are null
→
Fix
Reproduce outside the container: instantiate the class with new in a plain unit test, passing mocks by hand. If a field is null there, it was field-injected and invisible. Convert to constructor injection with final fields so the compiler enforces the dependency, then re-run ./mvnw test to confirm the context test passes too.
Could Not Autowire — Causes Compared
Root CauseHow to ConfirmFixPrevention
No qualifying bean registeredNoSuchBeanDefinitionException names the typeAdd stereotype or @Bean; fix scan gapsBoot-test the slice in CI
Multiple candidates, no tiebreakNoUniqueBeanDefinitionException lists beans@Qualifier at point or @Primary defaultOne default decided at code review
Component-scan package gapBean missing from actuator beans listMove class or set scanBasePackagesKeep app class above components
Constructor circular dependencyBeanCurrentlyInCreation in nested chainBreak cycle via events or providerConstructor injection plus ArchUnit rule
⚙ Quick Reference
3 commands from this guide
FileCommand / CodePurpose
@ServiceNo Qualifying Bean vs Multiple Candidates
@Service("stripeBilling")Fixing Ambiguity with @Qualifier at the Injection Point
@SpringBootApplication // scans com.acme.** by defaultComponent-Scan Gaps

Key takeaways

1
Read the nested Caused by chain
it names the missing type and injection point.
2
Zero candidates means registration or scan gaps; many means ambiguity.
3
@Qualifier picks per injection point; @Primary sets the global default.
4
Keep the app class above components so scanning covers everything.
5
Break constructor cycles with events or providers, not field injection.
6
Standardize on constructor injection with final fields.

Common mistakes to avoid

5 patterns
×

Adding @Qualifier before checking whether any bean exists

Symptom
Qualifier on a missing bean still fails, now with a longer error mentioning the qualifier name.
Fix
Read the nested Caused by chain first — it names the missing type and the injection point. Then check registration before changing annotations.
×

Creating beans in packages outside the component-scan root

Symptom
The class has @Service and the injection looks right, yet startup insists no bean exists.
Fix
Move the class under the scanned root or add explicit @ComponentScan base packages. Verify with the beans endpoint.
×

Adding a second implementation without updating injection points

Symptom
Startup breaks the moment the new class is added, with NoUniqueBeanDefinitionException naming both.
Fix
Annotate one implementation with @Primary or qualify each injection point. Decide the default once, explicitly.
×

Fixing constructor cycles with field injection

Symptom
Startup passes but tests turn painful: null fields, reflection setup, and hidden required dependencies.
Fix
Replace the cycle with constructor injection plus an event, a provider, or a setter for the back-reference. Keep constructors acyclic.
×

Sprinkling @Autowired on fields, setters, and constructors

Symptom
Injection behavior differs between Spring and plain unit tests, and required dependencies aren't obvious.
Fix
Keep @Autowired on constructors only, make fields final, and let the compiler enforce required dependencies.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How do you tell no-qualifying-bean apart from multiple-candidates?
Q02SENIOR
A @Service class exists but Spring reports no bean. Walk me through the ...
Q03SENIOR
When do you use @Qualifier versus @Primary?
Q04SENIOR
Two services need each other. How do you resolve the constructor cycle?
Q05SENIOR
Why do teams ban field injection in favor of constructors?
Q01 of 05JUNIOR

How do you tell no-qualifying-bean apart from multiple-candidates?

ANSWER
No qualifying bean means zero candidates, usually a missing annotation or scan gap. Multiple candidates means two or more matched, needing @Qualifier or @Primary. The nested exception names which case.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What's the difference between UnsatisfiedDependencyException and NoSuchBeanDefinitionException?
02
Can I make an injection optional instead of failing startup?
03
When should I use @Bean instead of component scanning?
04
Which injection style should a new project standardize on?
05
Do integration tests catch wiring errors before production?
06
What exactly does @Primary change at runtime?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
✓ Verified
production tested
September 25, 2026
last updated
1,950
articles · all by Naren
🔥

That's Spring. Mark it forged?

6 min read · try the examples if you haven't

←
Previous
ADB Device Unauthorized Fix
5 / 5 · Spring