Databases, Spring and microservices · 2. Spring Core and Spring MVC, lesson 4 of 8

Java configuration: @Configuration, @Bean, @Import, @Profile and @Conditional

Intermediate3 min read@since 17Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

There are two ways to register beans:

  • Component scanning: annotate your own classes with @Component, @Service, @Repository or @Controller. @SpringBootApplication scans the package of the main class and everything below it.
  • @Bean methods in a @Configuration class: for classes you don't own (RestClient, ObjectMapper, a third-party SDK) or that need construction logic.

Important details:

  • In a @Configuration class (full mode), Spring proxies the class so that one @Bean method calling another returns the same singleton. @Configuration(proxyBeanMethods = false) (lite mode, used by Spring Boot's own auto-configuration) skips that proxy: inject dependencies as method parameters instead of calling the methods.
  • @Import pulls in other configuration classes; @ComponentScan customises scanning.
  • @Profile("dev") registers a bean only when that profile is active.
  • @Conditional and Spring Boot's @ConditionalOnProperty, @ConditionalOnClass and @ConditionalOnMissingBean decide at startup whether a bean is registered. That is how auto-configuration works, and why defining your own bean replaces Boot's default.

Example

Java
@Configuration
public class InfrastructureConfig {

    @Bean
    Clock clock() {
        return Clock.system(ZoneId.of("Asia/Kolkata"));      // inject Clock instead of calling now() directly
    }

    @Bean
    RestClient paymentsClient(RestClient.Builder builder, @Value("${payments.url}") String url) {
        return builder.baseUrl(url).build();                 // a class we don't own: configure it here
    }

    @Bean
    @Profile("dev")
    DemoDataLoader demoData(CourseRepository courses) {      // only when the "dev" profile is active
        return new DemoDataLoader(courses);
    }

    @Bean
    @ConditionalOnProperty(name = "features.sms", havingValue = "true")
    Notifier smsNotifier(SmsClient client) {                 // only if features.sms=true
        return new SmsNotifier(client);
    }
}
Full vs lite mode
@Configuration                                   // full mode: the class is proxied
class ReportsConfig {
    @Bean Clock clock() { return Clock.systemUTC(); }
    @Bean ReportService reports() {
        return new ReportService(clock());          // returns the SAME Clock bean, thanks to the proxy
    }
}

@Configuration(proxyBeanMethods = false)         // lite mode: faster startup, no proxy
class ReportsConfigLite {
    @Bean Clock clock() { return Clock.systemUTC(); }
    @Bean ReportService reports(Clock clock) {      // take it as a parameter; calling clock() would create a new one
        return new ReportService(clock);
    }
}

Common mistake

Calling one @Bean method from another in a lite-mode configuration (or a @Component class): you get a brand-new object instead of the singleton.

Under the hood

Spring Boot's auto-configuration classes are listed in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports and guarded by conditions. Run with --debug (or check the Actuator conditions endpoint) to see which ones matched and why. Most of them use @ConditionalOnMissingBean, so declaring your own bean of the same type quietly switches Boot's version off.

Check yourself

Which packages does @SpringBootApplication scan by default?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of Spring Core and Spring Security in depth.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.