Beyond annotating your own classes
Component scanning handles your own classes beautifully — but real applications also need to wire third-party types, build objects with logic, register beans conditionally, and pull in external configuration. That's what Java-based bean configuration is for. This article covers the toolkit interviewers expect you to know.
@Configuration and @Bean
@Configuration marks a class as a source of bean definitions; @Bean marks a method whose return
value becomes a managed bean:
@Configuration
class AppConfig {
@Bean
PaymentGateway paymentGateway() { // method name = bean name
return new StripeGateway();
}
}
A @Bean method declares its own dependencies as parameters, which Spring autowires:
@Bean
OrderService orderService(PaymentGateway gateway, InventoryRepo repo) {
return new OrderService(gateway, repo); // gateway & repo injected from the container
}
Prefer parameters over calling sibling @Bean methods to fetch collaborators.
Why @Configuration matters: full vs lite mode
@Configuration classes are CGLIB-proxied so that inter-bean method calls return the shared
singleton. The same @Bean methods inside a plain @Component are not:
@Configuration // full mode: a() returns the SAME bean each call
class GoodConfig {
@Bean A a() { return new A(); }
@Bean B b() { return new B(a()); } // a() → the singleton A
}
@Component // lite mode: a() builds a NEW A each call!
class RiskyConfig {
@Bean A a() { return new A(); }
@Bean B b() { return new B(a()); } // duplicate A — bypasses the container
}
Always host inter-referencing @Bean methods in @Configuration.
Lifecycle callbacks
Hook a bean's init/destroy three ways. For library types you can't annotate, use the @Bean
attributes:
@Bean(initMethod = "start", destroyMethod = "stop")
ConnectionPool pool() { return new ConnectionPool(); }
For your own classes, annotations are cleaner:
@Component
class Cache {
@PostConstruct void warmUp() { } // after injection
@PreDestroy void flush() { } // before context close
}
Spring even infers a close/shutdown destroy method for AutoCloseable beans. Remember:
destroy callbacks don't fire for prototype-scoped beans.
@Value: inject individual settings
@Value injects externalized configuration — properties, env vars, or SpEL:
@Service
class MailService {
@Value("${mail.host}") private String host; // from properties
@Value("${mail.port:25}") private int port; // with default
MailService(@Value("${mail.from}") String from) { } // constructor injection
}
${...} resolves against the Environment; a colon gives a default. For a handful of settings this
is fine — but once a component reads a whole group of related keys, switch to typed properties.
@ConfigurationProperties: bind a whole group
@ConfigurationProperties binds a cohesive set of properties to a typed object, with relaxed
binding, nested objects, collections, and validation:
@ConfigurationProperties(prefix = "mail")
@Validated
@Component
class MailProperties {
@NotBlank private String host;
@Min(1) @Max(65535) private int port = 25;
private List<String> recipients; // binds mail.recipients[0], [1], ...
// getters/setters
}
mail.host=smtp.example.com
mail.port=587
mail.recipients[0][email protected]
With @Validated and spring-boot-starter-validation, bad configuration fails the app at
startup with a clear message naming the offending property — far better than discovering it in
production. This fail-fast validation is a key reason to prefer typed properties over scattered
@Values.
Conditional bean registration
@ConditionalOnProperty registers a bean only when a property has a given value — a feature toggle
with no code branches:
@Bean
@ConditionalOnProperty(name = "features.cache.enabled", havingValue = "true")
CacheManager cacheManager() { return new CaffeineCacheManager(); }
For logic the built-in conditions can't express, implement Condition:
class OnLinuxCondition implements Condition {
public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
}
@Bean
@Conditional(OnLinuxCondition.class)
NativeService nativeService() { return new NativeService(); }
ConditionContext exposes the environment, bean registry, and classpath. Every @ConditionalOnX
annotation in Spring Boot is just a pre-built Condition.
SpEL: #{...} vs ${...}
Two syntaxes are constantly confused. ${...} is a property placeholder; #{...} is a SpEL
expression:
@Value("#{2 * 1024 * 1024}") // SpEL arithmetic → 2097152
private int bufferBytes;
@Value("#{systemProperties['user.region'] ?: 'us'}") // env + elvis default
private String region;
@Value("${mail.host}") // property placeholder
private String host;
SpEL supports method calls, bean references, collection projection, and ternary/elvis operators — but keep it simple. Business logic belongs in code, not annotations.
Composing configuration and avoiding name clashes
@Import assembles modular @Configuration classes (and underpins custom @Enable* annotations):
@Configuration
@Import({ SecurityConfig.class, WebConfig.class })
class AppConfig { }
And note: in Spring Boot, two beans with the same name fail by default —
BeanDefinitionOverrideException at startup. The fix is to rename, qualify, or use
@ConditionalOnMissingBean, not to flip spring.main.allow-bean-definition-overriding=true.
Java config vs component scanning
The two approaches complement each other:
@Service class OrderService { } // scanning — your classes, zero boilerplate
@Configuration
class Config {
@Bean ObjectMapper objectMapper() { // @Bean — third-party type, custom build
return new ObjectMapper().findAndRegisterModules();
}
}
Scan your own components for brevity; write explicit @Bean methods for library types and anything
needing construction logic. Every real app blends the two.
Recap
Define beans with @Configuration + @Bean (full mode, so inter-bean calls return singletons),
hook lifecycle with @PostConstruct/@PreDestroy or the @Bean attributes, and pull in
configuration with @Value for single settings and @ConfigurationProperties for validated
groups. Gate optional beans with @ConditionalOnProperty or a custom Condition, compose modules
with @Import, and remember that duplicate bean names fail by design. Component scanning and Java
config aren't rivals — use scanning for your code and @Bean methods for everything you can't
annotate.