Spring NoSuchBeanDefinition — Bean Never Registered
Spring NoSuchBeanDefinitionException: bean never registered.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Spring Boot basics
- ✓Dependency injection concepts
- ✓Reading startup logs
- NoSuchBeanDefinitionException means the container has no bean of the required type — the class exists in code but was never registered
- Top causes: missing @Component/@Service annotation, class outside scanned packages, @Bean name mismatch, or absent starter
- Default scanning covers only packages below the application class — sibling packages are invisible
- Multiple candidates need @Qualifier at the injection point or @Primary on the default bean
- The condition evaluation report lists every candidate and exclusion — read it before changing code
Think of Spring's container as a company directory. Writing a class is like hiring a person; registering a bean is adding them to the directory. NoSuchBeanDefinitionException means someone asked the directory for BillingService and got no listing — the person exists in the building (the code compiles) but was never added to the directory (no annotation, wrong department, wrong name spelling). The fix is never hiring twice; it is correcting the directory entry.
The application refuses to start: No qualifying bean of type com.example.BillingService available. The class exists — you wrote it yesterday, it compiles, the IDE autocompletes it. And yet Spring insists there is no such bean, with startup halted and a wall of condition-report text.
This exception is Spring's way of saying the container looked for a bean and found nothing matching. Registration is explicit in Spring: a class becomes a bean through a stereotype annotation, an @Bean method, or auto-configuration — and only if component scanning actually visits its package. Miss any link and the type exists in code but not in the container.
The frustration comes from the gap between code truth and container truth. The IDE sees classes; Spring sees registered definitions. Developers debug the class (correct) while the fix lives in the registration (annotation, package, name) — a different file, sometimes a different module.
This article closes that gap: how scanning, stereotypes, @Bean names, qualifiers, and auto-configuration conditions decide what exists. You will learn to read the expected-bean message, wield the condition report, and structure packages so this error stops appearing.
Missing Stereotypes: Annotated for Humans, Invisible to Spring
A class becomes a Spring bean through registration: a stereotype annotation (@Component and its specializations @Service, @Repository, @Controller), an @Bean factory method, or framework auto-configuration. Without one of these, the class is invisible to the container no matter how correct it is — compilation and container membership are independent facts.
The specializations carry identical scanning behavior with added semantics. @Service marks business logic, @Controller marks web handlers, and @Repository adds persistence-exception translation for data access. Choosing the honest stereotype documents the layer while registering the bean — one annotation doing two jobs.
The failure mode is silence: an unannotated class produces no warning at startup, only the downstream NoSuchBeanDefinitionException at the injection point. Nothing points back at the missing annotation except the zero-candidate listing. That indirection is why missing stereotypes survive code review — reviewers verify logic, not container metadata.
The prevention is a review rule with teeth: every injectable class carries a visible stereotype, and context-load tests prove registration in CI. A test that boots the module context fails on a missing annotation in seconds, converting a deploy-night mystery into a pull-request comment.
Scan Roots: Why Sibling Packages Are Invisible
Component scanning starts from a root: by default, the package of the application class. Every sub-package is visited; sibling packages are not. A service moved from com.example.billing to com.shared.billing crosses an invisible wall — annotated, compiled, and unscanned.
The application class placement is therefore architecture, not housekeeping. Teams that nest it two levels deep for tidiness shrink their scan root with every level, and each future package move gambles on invisible coverage. The safest layout puts the application class at the package root so the default scan covers the entire codebase by construction.
Explicit scanBasePackages documents intent when the layout cannot be flat. Multi-module projects with shared libraries name each root deliberately, turning invisible defaults into reviewed configuration. The attribute costs one line and ends all package-roulette incidents.
Verify coverage mechanically. The condition report lists scanned candidates; a missing class there means the scanner never visited it. When a bean vanishes after a move, the report — not the annotations — is the first document to open. Pair the rule with ArchUnit-style tests that fail builds on uninjected concrete dependencies.
@Bean Names: Method Names Are Bean Names
@Bean methods register under their method name by default: paymentGateway() above creates a bean named paymentGateway. Injection by name — @Qualifier("paymentGateway") or @Resource — must spell that exact name, and constructor parameters in recent Spring versions also bind by parameter name. One rename breaks all three silently.
The mismatch arrives through ordinary refactors. Renaming the method for readability, splitting one bean into two, or copying a configuration without updating injection points all produce zero-candidate failures naming a bean that looks like it should exist. It does exist — under its old name.
Explicit naming removes the coupling between method readability and bean identity: @Bean("payments") fixes the name regardless of method renames. Qualified injection points then bind to a stable contract instead of a Java identifier that style guides may rewrite.
When the message names an expected bean, search configurations for that exact string before theorizing. The bean definition is a line of code with a name; matching strings beats architectural speculation every time. Sketch the scan tree in onboarding docs so new packages land in covered ground, and review every application-class move as an architecture change.
Two Candidates, One Point: @Qualifier and @Primary
When two beans share a type — Stripe and PayPal gateways, primary and replica data sources — type-only injection cannot choose. Spring fails rather than guesses, listing both candidates in an expected-single-but-found-two message. The failure is the framework refusing to make your decision.
@Qualifier decides at the injection point by naming the wanted bean. It keeps both candidates registered and selects per consumer: checkout takes Stripe, refunds take PayPal, each explicit. Qualifier values are strings, so constants beat literals for typo resistance.
@Primary decides globally by marking the default candidate. Unqualified injection points receive the primary; qualified points still get their named bean. One primary per type keeps the default unambiguous — two primaries of one type fail as loudly as two unmarked candidates.
Prefer constructor injection throughout, because its failure messages list every candidate with types and names at startup. Field injection fails at the same point with less context, and setter injection defers some failures to first use. Fail-fast clarity is worth the extra constructor lines. Record bean names in module READMEs where cross-team injection depends on them.
Auto-Configuration Conditions: Silent Gates on Framework Beans
Auto-configuration creates framework beans — DataSources, templates, mappers — when conditions hold: a class on the classpath, a property set, another bean absent. Each condition is a silent gate, and a changed dependency or property flips gates without touching application code.
The condition evaluation report is the map of those gates. Rerun with debug enabled and search for the auto-configuration class: the report shows which @ConditionalOnClass found its class, which @ConditionalOnProperty matched, and which @ConditionalOnMissingBean deferred. The exclusion reason is printed plainly — no inference needed.
Missing starters are the common trigger. Upgrading or trimming dependencies removes the class an auto-configuration waited on, and its beans vanish together. The error names infrastructure types rather than application code, which sends teams searching their own classes for a dependency problem.
Property gates deserve equal suspicion. Management endpoints, security filters, and data features toggle on properties; a renamed or deleted property silently unregisters whole bean families. Diff properties alongside dependencies in every upgrade review. Standardize qualifier constants in a shared class to eliminate string drift.
Slice Tests and Module Context Tests That Prevent It
Test slices (@WebMvcTest, @DataJpaTest) load partial contexts for speed, and partial contexts exclude beans by design. A service injected in production but absent in the slice fails with the same exception as a genuinely missing bean — same message, opposite meaning.
The fix matches the intent: @Import the needed configuration into the slice, mock the boundary with @MockBean, or test against the full context when the integration is the point. Each option declares what the test covers instead of pretending the slice is the app.
Module context tests are the systematic prevention. A @SpringBootTest per module that merely boots its context catches missing registrations, broken scans, and condition surprises in CI within seconds. They are the cheapest tests with the highest registration value.
Keep a simple rule for the team: a bean used in production must appear in at least one booting test. Untested registrations are unregistered until proven otherwise — the container's opinion at deploy time is the only one that counts. Version the condition report diff inside upgrade pull requests so reviewers see gate changes, and keep a starter-to-bean map for key dependencies so missing starters point at exact artifacts.
A Package Move Deleted a Bean and Halved the Fleet for 40 Minutes
- Package moves are behavior changes in Spring — default scanning makes directory layout a runtime contract, not cosmetics.
- Every module needs a context-load test; untested modules hide registration breaks until deploy.
- Keep the application class at the package root so the default scan covers everything by construction.
| File | Command / Code | Purpose |
|---|---|---|
| BillingService.java | @Service | Missing Stereotypes |
| ShopApplication.java | @SpringBootApplication(scanBasePackages = "com.example") | Scan Roots |
| ClientConfig.java | @Configuration | @Bean Names |
| CheckoutService.java | @Service | Two Candidates, One Point |
Key takeaways
Common mistakes to avoid
5 patternsForgetting the stereotype annotation entirely
Placing the bean outside the scanned packages
Injecting by a name that matches no @Bean method
Two beans of one type with no qualifier
Missing the starter that auto-creates the bean
Interview Questions on This Topic
What does NoSuchBeanDefinitionException mean?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring. Mark it forged?
5 min read · try the examples if you haven't