Spring BootComponent Scanning
Spring Boot

Component Scanning

Component scanning is the mechanism Spring uses to automatically discover and register beans. Instead of declaring every bean manually, you annotate classes with stereotypes like @Component, @Service, @Repository, or @Controller, and Spring finds and registers them automatically. Understanding how scanning works — its rules, filters, and performance implications — lets you control exactly what Spring discovers.

How Component Scanning Works

Component scanning is triggered by @ComponentScan — included automatically via @SpringBootApplication. Spring walks the specified base packages, examines every class file, and registers any class annotated with a component stereotype as a Spring bean. The scanning process: 1. Spring loads class metadata (using ASM bytecode reader — classes are NOT loaded by the JVM yet) 2. It checks each class for @Component or any annotation meta-annotated with @Component 3. If found, a BeanDefinition is created and registered with the BeanFactory 4. After all definitions are registered, beans are instantiated and wired This two-phase approach (scan then instantiate) means Spring can resolve all dependencies before creating any bean — allowing it to detect circular dependencies and determine creation order.

Stereotype Annotations

Spring provides four stereotype annotations that trigger component scanning registration. Each is meta-annotated with @Component — they all register beans the same way but communicate different roles and may carry additional behavior.
Java
// @Component — generic Spring-managed component:
// Use when no more specific stereotype applies
@Component
public class EmailTemplateRenderer {
    public String render(String template, Map<String, Object> variables) {
        // render template
        return "";
    }
}

// @Service — business logic / service layer:
// Semantically identical to @Component
// Communicates intent: this class contains business logic
// May receive additional Spring features in future versions
@Service
public class UserService {
    public User createUser(CreateUserRequest request) { return null; }
}

// @Repository — data access / persistence layer:
// Adds exception translation: database-specific exceptions
// (SQLException, HibernateException) are translated to
// Spring's DataAccessException hierarchy automatically
// This makes your service layer independent of the persistence technology
@Repository
public class JdbcUserRepository implements UserRepository {
    public User save(User user) { return null; }
}

// @Controller — web MVC controller (returns view names):
@Controller
public class HomeController {
    @GetMapping("/")
    public String home(Model model) {
        return "home";   // resolves to templates/home.html
    }
}

// @RestController — REST API controller:
// Meta-annotated with @Controller + @ResponseBody
// Return values serialized to JSON/XML automatically
@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping("/{id}")
    public UserResponse getUser(@PathVariable Long id) { return null; }
}

// @Configuration — configuration class:
// Also detected by component scanning
// Methods annotated with @Bean register additional beans
@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

// ALL of these are ultimately @Component:
// Spring treats them identically for bean registration purposes
// The stereotype name communicates intent and enables tooling support

@ComponentScan — Controlling What's Scanned

@ComponentScan configures which packages to scan, what to include, and what to exclude. @SpringBootApplication includes @ComponentScan for the root package — usually all you need.
Java
// @SpringBootApplication includes @ComponentScan for the annotated class's package:
@SpringBootApplication   // scans com.example.myapp and all sub-packages
public class MyappApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyappApplication.class, args);
    }
}

// Explicit @ComponentScan — customize the scan:
@Configuration
@ComponentScan(basePackages = {
    "com.example.myapp",          // your application
    "com.example.shared"          // shared library
})
public class AppConfig { }

// @ComponentScan with type-safe base package classes:
// Specify a class IN the package you want to scan — refactoring-safe:
@ComponentScan(basePackageClasses = {
    MyappApplication.class,     // scans com.example.myapp
    SharedLibraryMarker.class   // scans com.example.shared
})
public class AppConfig { }

// Multiple @ComponentScan (Java 8+ repeatable annotations):
@ComponentScan("com.example.myapp")
@ComponentScan("com.example.shared")
public class AppConfig { }

// Customize via @SpringBootApplication:
@SpringBootApplication(
    scanBasePackages = {"com.example.myapp", "com.example.shared"},
    scanBasePackageClasses = {MyappApplication.class}
)
public class MyappApplication { }

// CRITICAL: The @SpringBootApplication class must be in the ROOT package:
// com/example/myapp/MyappApplication.java → scans com.example.myapp.*
// If placed in a sub-package, sibling packages are missed:
// com/example/myapp/config/MyappApplication.java → only scans config package!

Include and Exclude Filters

Component scanning filters let you include or exclude specific classes from scanning — by annotation, type, regex pattern, or custom logic. Filters give precise control over what Spring discovers.
Java
// includeFilters — scan additional classes beyond the default:
@ComponentScan(
    basePackages = "com.example",
    includeFilters = {
        // Include classes annotated with @MyCustomAnnotation:
        @ComponentScan.Filter(
            type = FilterType.ANNOTATION,
            classes = MyCustomAnnotation.class
        ),
        // Include classes that are subtypes of a specific type:
        @ComponentScan.Filter(
            type = FilterType.ASSIGNABLE_TYPE,
            classes = BaseRepository.class
        )
    },
    useDefaultFilters = false  // disable default @Component detection
    // Now ONLY classes matching includeFilters are scanned
)
public class CustomScanConfig { }

// excludeFilters — skip specific classes during scanning:
@ComponentScan(
    basePackages = "com.example",
    excludeFilters = {
        // Exclude classes annotated with @Generated:
        @ComponentScan.Filter(
            type = FilterType.ANNOTATION,
            classes = Generated.class
        ),
        // Exclude a specific class:
        @ComponentScan.Filter(
            type = FilterType.ASSIGNABLE_TYPE,
            classes = LegacyService.class
        ),
        // Exclude by regex pattern on class name:
        @ComponentScan.Filter(
            type = FilterType.REGEX,
            pattern = ".*Legacy.*"
        ),
        // Exclude by AspectJ type pattern:
        @ComponentScan.Filter(
            type = FilterType.ASPECTJ,
            pattern = "com.example..*Legacy*"
        )
    }
)
public class AppConfig { }

// Custom filter — exclude classes in a specific sub-package:
public class ExcludeTestPackageFilter implements TypeFilter {

    @Override
    public boolean match(MetadataReader metadataReader,
                         MetadataReaderFactory factory) throws IOException {
        String className = metadataReader.getClassMetadata().getClassName();
        return className.contains(".test.");  // true = exclude this class
    }
}

@ComponentScan(
    basePackages = "com.example",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.CUSTOM,
        classes = ExcludeTestPackageFilter.class
    )
)
public class AppConfig { }

// In Spring Boot tests — limit what's scanned:
@SpringBootTest
@ComponentScan(
    basePackages = "com.example.myapp",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE,
        classes = {ScheduledTasks.class, KafkaConsumer.class}
    )
)
class ServiceLayerTest { }

Custom Stereotype Annotations

You can create your own stereotype annotations by meta-annotating with @Component. Custom stereotypes carry additional metadata, enable consistent configuration, and communicate domain-specific roles.
Java
// Custom stereotype — @DomainService:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service                    // meta-annotated with @Service — detected by component scan
@Transactional              // all DomainService beans get transactions by default
public @interface DomainService {
    String value() default "";
}

// Use the custom stereotype:
@DomainService
public class OrderDomainService {
    // Registered as a Spring bean automatically
    // Has @Transactional applied automatically
    // Communicates: this is a domain service, not an application service
    public Order placeOrder(PlaceOrderCommand command) { return null; }
}

// Custom stereotype — @UseCase (hexagonal architecture):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
@Transactional
public @interface UseCase { }

@UseCase
public class RegisterUserUseCase {
    public void execute(RegisterUserCommand command) { }
}

// Custom stereotype — @Adapter (hexagonal architecture):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Adapter { }

@Adapter
public class PostgresUserRepository implements UserRepository {
    public User save(User user) { return null; }
}

// Custom stereotype — @EventHandler with ordering:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface EventHandler {
    int order() default 0;
}

@EventHandler(order = 1)
public class AuditEventHandler {
    @EventListener
    public void handle(UserRegisteredEvent event) { }
}

// Custom stereotype — @WebAdapter for REST controllers:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping
public @interface WebAdapter {
    String value() default "";
}

@WebAdapter("/api/users")
public class UserWebAdapter {
    @GetMapping("/{id}")
    public UserResponse getUser(@PathVariable Long id) { return null; }
}

Scanning Performance and Lazy Scanning

Component scanning reads every class file in the specified packages. On large classpaths, this can slow startup. Spring Boot 2.2+ introduced lazy initialization and scanning optimizations.
Java
// Performance impact of scanning:
// Spring uses ASM bytecode reader — classes are NOT loaded by the JVM during scanning
// Only class metadata is read from .class files
// Still: reading thousands of .class files takes time

// Measure scanning cost:
// application.properties:
// logging.level.org.springframework.context.annotation=DEBUG
// Prints each discovered component — helps identify unexpected inclusions

// Optimize scanning — be specific about packages:
// BAD: scan the entire classpath root
@ComponentScan("com")   // scans EVERY class in every com.* package

// GOOD: scan only your application packages
@ComponentScan("com.example.myapp")   // only your code

// Spring Boot index — pre-compute component scan results at compile time:
// Add to pom.xml:
// groupId: org.springframework
// artifactId: spring-context-indexer
// optional: true
// Generates META-INF/spring.components at compile time
// Spring uses the index at startup instead of scanning — much faster for large apps

// Lazy initialization — defer ALL bean creation until first use:
// application.properties:
// spring.main.lazy-initialization=true
// Reduces startup time dramatically
// First request to any endpoint will be slower (beans created on demand)

// Selective lazy initialization — keep critical beans eager:
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        app.setLazyInitialization(true);   // all beans lazy by default
        app.run(args);
    }
}

// Override lazy for specific beans that MUST be eager:
@Component
@Lazy(false)   // eager even when global lazy is enabled
public class DatabaseMigrationRunner {
    @PostConstruct
    public void migrate() {
        // Must run at startup, before any requests
    }
}

// Spring Boot 2.4+ — startup actuator endpoint shows scanning time:
// GET /actuator/startup
// Shows timeline of each startup step including component scan duration

Scanning in Multi-Module Projects

Multi-module Maven/Gradle projects require careful attention to package structure and scan configuration. Each module's components must be reachable from the root scan package.
Java
// Multi-module project structure:
// my-app/
//   myapp-core/          → com.example.core
//   myapp-web/           → com.example.web
//   myapp-data/          → com.example.data
//   myapp-boot/          → com.example.boot  (entry point module)

// Option 1 — Shared root package (simplest):
// All modules use sub-packages of com.example:
// com.example.core.service.UserService
// com.example.data.repository.UserRepository
// com.example.web.controller.UserController

// @SpringBootApplication in com.example scans all sub-packages:
package com.example.boot;

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

// Option 2 — Separate packages, explicit scan:
@SpringBootApplication(scanBasePackages = {
    "com.example.boot",
    "com.example.core",
    "com.example.web",
    "com.example.data"
})
public class MyAppApplication { }

// Option 3 — spring.factories / AutoConfiguration.imports in each module:
// Each module provides its own auto-configuration
// Main app picks it up via @EnableAutoConfiguration
// Scales well — adding a module is adding a dependency, no scan changes

// Option 4@Import in the main @Configuration:
@SpringBootApplication
@Import({
    CoreModuleConfig.class,   // imports all beans from core module
    DataModuleConfig.class,   // imports all beans from data module
})
public class MyAppApplication { }

// @Configuration in each module:
@Configuration
@ComponentScan("com.example.core")
public class CoreModuleConfig { }

@Configuration
@ComponentScan("com.example.data")
public class DataModuleConfig { }

// Testing individual modules without the full app:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = CoreModuleConfig.class)
class CoreModuleTest {
    @Autowired UserService userService;  // only core beans loaded
}

Conditional Component Registration

Components can be conditionally registered during scanning using @Conditional annotations. This lets you include or exclude beans based on the environment, classpath, properties, or any custom condition.
Java
// @ConditionalOnProperty — register only when property is set:
@Service
@ConditionalOnProperty(
    name = "app.feature.payments",
    havingValue = "true",
    matchIfMissing = false
)
public class PaymentProcessingService {
    // Only registered when app.feature.payments=true in application.properties
}

// @ConditionalOnClass — register only when a class is on the classpath:
@Service
@ConditionalOnClass(name = "com.stripe.Stripe")
public class StripeIntegrationService {
    // Only registered when the Stripe SDK is on the classpath
}

// @ConditionalOnMissingBean — register only when no other bean of this type exists:
@Service
@ConditionalOnMissingBean(PaymentService.class)
public class DefaultPaymentService implements PaymentService {
    // Default implementation — replaced if a custom PaymentService is defined
}

// @Profile — register only for specific profiles:
@Service
@Profile("dev")
public class MockEmailService implements EmailService {
    public void send(String to, String subject, String body) {
        System.out.println("MOCK EMAIL to: " + to + " | " + subject);
    }
}

@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
    public void send(String to, String subject, String body) {
        smtpClient.sendEmail(to, subject, body);
    }
}

// Custom @Conditional — register based on any logic:
public class OnLinuxCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return System.getProperty("os.name").toLowerCase().contains("linux");
    }
}

@Service
@Conditional(OnLinuxCondition.class)
public class LinuxSpecificService {
    // Only registered when running on Linux
}

// Combining conditions:
@Service
@Profile("prod")
@ConditionalOnProperty(name = "feature.analytics", havingValue = "true")
@ConditionalOnClass(name = "com.example.analytics.AnalyticsClient")
public class ProductionAnalyticsService {
    // Registered ONLY when: prod profile AND property set AND class available
}