@Configuration & @Bean
Java-based configuration lets you define beans in code instead of XML, giving you a type-safe, refactor-friendly way to register objects the container should manage. A @Configuration class groups @Bean factory methods, and Spring treats it specially so that calls between those methods still return the managed singleton — not a fresh object.
Why Java configuration
Stereotype annotations like @Service work great for classes you own. But you often need beans for types you cannot annotate — a RestClient, a DataSource, an ObjectMapper from a third-party library. Java config solves this: you write a method that constructs the object and mark it @Bean, and the container manages the return value. This complements the component scanning described in Stereotype Annotations.
@Bean factory methods
A @Bean method’s return value becomes a bean. By default the method name is the bean name, and the bean is a singleton.
@Configuration
public class AppConfig {
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://api.example.com")
.build();
}
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules();
}
}
You can rename or alias a bean and control lifecycle callbacks:
@Bean(name = "primaryMapper", initMethod = "init", destroyMethod = "close")
public ReportEngine reportEngine() {
return new ReportEngine();
}
Dependencies between beans
Beans frequently depend on one another. There are two idiomatic ways to wire them.
Method parameters (preferred): declare the dependency as a parameter and Spring injects the matching bean.
@Configuration
public class ServiceConfig {
@Bean
public PricingService pricingService() {
return new PricingService(0.2);
}
@Bean
public CheckoutService checkoutService(PricingService pricingService) {
return new CheckoutService(pricingService);
}
}
Method calls (inter-bean reference): call one @Bean method from another. This reads naturally, and thanks to proxying it still returns the same singleton rather than a new instance.
@Configuration
public class ServiceConfig {
@Bean
public PricingService pricingService() {
return new PricingService(0.2);
}
@Bean
public CheckoutService checkoutService() {
return new CheckoutService(pricingService()); // returns the managed singleton
}
}
Note: The method-call style only returns the shared singleton because Spring subclasses the
@Configurationclass with a CGLIB proxy. That behavior is controlled byproxyBeanMethods.
proxyBeanMethods: full vs lite mode
@Configuration has a proxyBeanMethods attribute, and it changes how inter-bean method calls behave.
proxyBeanMethods = true (full mode, default) | proxyBeanMethods = false (lite mode) | |
|---|---|---|
| CGLIB proxy created | Yes | No |
| Inter-bean method call returns | The shared singleton | A new object every call |
| Startup cost | Slightly higher | Lower (no proxy class) |
Safe to call @Bean methods directly | Yes | No — avoid it |
| Use when | Methods reference each other | Methods are self-contained |
In full mode (the default), the proxy intercepts every @Bean method call and routes it through the container:
@Configuration // proxyBeanMethods = true by default
public class FullModeConfig {
@Bean
public Cache cache() {
return new Cache();
}
@Bean
public Service service() {
// cache() returns the SAME singleton the container holds
return new Service(cache());
}
}
In lite mode, no proxy is created, so calling cache() directly runs the raw method and builds a brand-new Cache — usually a bug. Use lite mode only when methods do not call each other, or wire dependencies via parameters instead:
@Configuration(proxyBeanMethods = false)
public class LiteModeConfig {
@Bean
public Cache cache() {
return new Cache();
}
@Bean
public Service service(Cache cache) { // inject via parameter — safe
return new Service(cache);
}
}
Tip: Spring Boot’s own auto-configuration classes use
proxyBeanMethods = falseto speed up startup. Adopt the same pattern for your config classes when bean methods don’t reference each other — just remember to use method parameters for dependencies.
@Import — composing configurations
@Import pulls additional @Configuration classes (or plain components) into the context. It is useful for assembling modular configuration or for enabling a feature explicitly rather than relying on scanning.
@Configuration
@Import({DataConfig.class, SecurityConfig.class})
public class ApplicationConfig { }
This registers everything defined in DataConfig and SecurityConfig even if those classes live outside the component-scan path. Many @Enable* annotations (such as @EnableScheduling) work by importing configuration under the hood.
@Bean vs stereotypes
// Use a stereotype when you own and can annotate the class:
@Service
public class InvoiceService { }
// Use @Bean when you cannot annotate the type:
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
Both produce singletons managed identically by the container; the choice is about whether you control the source.
Best Practices
- Prefer
@Beanmethod parameters over inter-bean method calls — it works in both proxy modes and reads clearly. - Keep the default
proxyBeanMethods = truewhen methods call each other; switch tofalseonly when they’re independent. - Use stereotypes for classes you own; reserve
@Beanfor third-party or infrastructure types. - Group related beans into focused configuration classes and assemble them with
@Import. - Avoid mutable shared state in beans created by factory methods, just as with any singleton.