Have you encountered a NoSuchBeanDefinitionException saying “○○Service not found” and wondered, “But I did add @Service”? In many cases, the cause isn’t how the Bean is written, but where it’s placed (the distinction between @Service, @Repository, and @Controller is covered in this article).

This article organizes the mechanism by which Beans placed in a separate package or module fall outside component scanning, along with three resolution patterns: specifying scanBasePackages, using an explicit @ComponentScan, and multi-module configurations.

The Cause: “Only Scans Under the Main Class” Specification

@SpringBootApplication implicitly includes @ComponentScan internally, and by default the scan range is limited to the package of the annotated class and its subpackages.

com.example.app
└── Application.java   ← @SpringBootApplication is here

com.example.lib
└── PaymentService.java  ← Even with @Service, it's outside the scan range

In this state, attempting to DI PaymentService produces an exception like the following:

org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'com.example.lib.PaymentService' available:
expected at least 1 bean which qualifies as autowire candidate.

Even if the class exists on the classpath, no Bean definition is created unless it’s included in the scan range (the role of @Component itself is explained in What is @Component?). What trips you up here isn’t “how it’s annotated” but “where it’s placed.”

The fix varies depending on your configuration, so first check which case applies to you.

  • If you’re placing Beans in a separate package within a single module → Pattern 1 or Pattern 2
  • If you want to separate configuration from the main class → Pattern 2
  • If the Bean is in a library module you depend on (a separate module declared via module dependency in build.gradle or pom.xml) → Pattern 3
  • If you’re unsure, compare your package structure diagram with the dependencies in build.gradle (or module dependencies in pom.xml)

Pattern 1: Expand the Scan Range with scanBasePackages

The easiest approach is to specify scanBasePackages on @SpringBootApplication.

@SpringBootApplication(scanBasePackages = {
    "com.example.app",
    "com.example.lib"
})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Since specifying package names as strings doesn’t follow refactoring, scanBasePackageClasses, which specifies via class references for type safety, is also an option.

@SpringBootApplication(scanBasePackageClasses = {
    Application.class,
    PaymentService.class
})
public class Application {
}

Be careful not to expand the range too much. Beans that were originally unrelated may end up referencing each other once they’re brought into the scan range, which can result in a circular reference error that prevents startup. Specify only as much as is needed.

Pattern 2: Control It with an Explicit @ComponentScan

If you don’t want to write configuration in the main class, separate @ComponentScan into a dedicated configuration class (the basic role of @Configuration is explained in What is @Configuration / @Bean?).

@Configuration
@ComponentScan(basePackages = "com.example.lib")
public class LibraryScanConfig {
}

Since @SpringBootApplication and @ComponentScan are different types, writing both on the same class will compile fine in Java itself. The problem occurs at startup. Because @SpringBootApplication already includes @ComponentScan as a meta-annotation, stacking your own @ComponentScan on top can cause attribute conflicts, resulting in an annotation configuration error at startup. Keep your custom scan configuration in a separate class.

Even if basePackages overlap, there’s no actual harm in scanning the same class twice. What you need to watch out for is when different classes in different packages have the same default Bean name. For example, if simple class names collide like com.example.app.Config and com.example.lib.Config, you’ll get a ConflictingBeanDefinitionException.

Pattern 3: Registering Beans in a Multi-Module Gradle/Maven Configuration

Since @SpringBootApplication doesn’t exist on the library module side, this can’t be resolved with scanBasePackages. The caller side needs to include the library’s package in its scan range.

// app/build.gradle
dependencies {
    implementation project(':payment-lib')
}
@SpringBootApplication(scanBasePackages = {
    "com.example.app",
    "com.example.paymentlib"
})
public class Application {
}

The drawback is that the caller side needs to know the library’s internal package name. If you want to avoid this, another approach is to prepare a @Configuration class on the library side and have the caller @Import it.

// payment-lib side
@Configuration
public class PaymentLibAutoConfig {
    @Bean
    public PaymentService paymentService() {
        return new PaymentService();
    }
}

The caller side only needs to add @Import(PaymentLibAutoConfig.class). Auto-configuration via spring.factories or AutoConfiguration.imports takes this mechanism a step further. For more details, see Understanding Spring Boot’s Auto-Configuration Mechanism.

Common Pitfalls

When @SpringBootTest doesn’t specify a configuration class, it searches upward from the test class’s package for a @SpringBootConfiguration (which @SpringBootApplication includes). If the test is placed shallowly, or if there’s a configuration where each module has its own separate @SpringBootApplication, the test may pick up a different configuration than the main one, causing Beans to be found only in the main application but not in the test. Slice tests like @WebMvcTest don’t perform component scanning at all, so Beans on the library side are out of scope to begin with.

Also, as a result of expanding the scan range, you might end up with multiple Beans of the same type being found, turning the error into NoUniqueBeanDefinitionException. This can be resolved with @Primary or @Qualifier.

For a general approach to diagnosing startup errors, see Diagnosing and Resolving Spring Boot Application Startup Failures.

Summary

Most NoSuchBeanDefinitionException issues are a matter of scan range, not how the Bean is written. For a simple configuration, use scanBasePackages; if you want to separate configuration, use @ComponentScan in a dedicated class; for multi-module setups, choose between adjusting the caller’s scan range or providing an @Import on the library side. Sketching out your package structure as a diagram will quickly tell you which pattern applies.