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
}