@Configuration marks a class as a source of bean definitions, and @Bean marks a
method whose return value becomes a container-managed bean. Together they're Spring's
Java-based configuration — the modern replacement for XML.
@Configuration
class AppConfig {
@Bean
PaymentGateway paymentGateway() { // method name = bean name "paymentGateway"
return new StripeGateway(); // returned object is managed by the container
}
}
Use @Bean factory methods when you need explicit construction logic or when the type is
a third-party class you can't annotate with @Component. The method's parameters are
themselves autowired, so @Bean methods can depend on other beans. This is the standard way
to configure infrastructure objects like DataSource, RestClient, or ObjectMapper.
Rule of thumb: @Configuration + @Bean = "build this object and let Spring manage it."
Reach for it whenever you can't (or don't want to) annotate the class itself.
A @Bean method declares its dependencies as method parameters, and Spring autowires
them from the container — exactly like constructor injection but for a factory method.
@Configuration
class ServiceConfig {
@Bean
OrderService orderService(PaymentGateway gateway, InventoryRepo repo) {
// 'gateway' and 'repo' are resolved from the container and passed in:
return new OrderService(gateway, repo);
}
}
Each parameter is resolved by type (with @Qualifier/@Value available on parameters too).
This is the preferred way for one bean to use another inside @Configuration — cleaner than
calling another @Bean method directly. Because parameters are autowired, the order in which
you declare @Bean methods doesn't matter; Spring sorts out the dependency graph.
Rule of thumb: Have a @Bean method take what it needs as parameters and let Spring
inject them — don't call sibling @Bean methods to get collaborators.
You can hook a bean's lifecycle three ways: the @Bean(initMethod, destroyMethod)
attributes, the @PostConstruct/@PreDestroy annotations, or implementing
InitializingBean/DisposableBean.
@Configuration
class ResourceConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
ConnectionPool pool() { // start() runs after creation, stop() on shutdown
return new ConnectionPool();
}
}
// For your own classes, annotations are usually cleaner:
@Component
class Cache {
@PostConstruct void warmUp() { } // after dependencies injected
@PreDestroy void flush() { } // before context closes
}
@Bean(initMethod=..., destroyMethod=...) is ideal for third-party classes with
lifecycle methods you can't annotate. For closeable types Spring even infers a destroy
method named close or shutdown automatically. @PostConstruct/@PreDestroy are best for
your own code. Note: destroy callbacks don't fire for prototype-scoped beans.
Rule of thumb: Annotate your own classes with @PostConstruct/@PreDestroy; use
@Bean(initMethod/destroyMethod) for library types you can't annotate.
@Value injects externalized configuration — property values, environment variables, or
SpEL expressions — into fields, constructor parameters, or @Bean method parameters.
@Service
class MailService {
@Value("${mail.host}") // from application.properties
private String host;
@Value("${mail.port:25}") // with a default if unset
private int port;
MailService(@Value("${mail.from}") String from) { } // constructor injection too
}
The ${...} syntax resolves against the Environment (properties, YAML, env vars, command
line). A colon provides a default (${key:fallback}). @Value also evaluates SpEL
via #{...}. For binding many related properties, prefer a typed @ConfigurationProperties
class over scattering @Value everywhere.
Rule of thumb: Use @Value("${key:default}") for a handful of individual settings;
switch to @ConfigurationProperties once a component reads a whole group of related keys.
@Value injects one property at a time; @ConfigurationProperties binds a whole group
of related properties to a typed object. For anything beyond a couple of values, the typed
approach wins.
@ConfigurationProperties(prefix = "mail")
@Component
class MailProperties {
private String host;
private int port = 25;
private List<String> recipients; // binds mail.recipients[0], [1], ...
// getters/setters (or use a record with constructor binding)
}
mail.host=smtp.example.com
mail.port=587
mail.recipients[0][email protected]
@ConfigurationProperties gives you type safety, relaxed binding (MAIL_HOST env →
mail.host), JSR-303 validation (@Validated), support for nested objects and
collections, and IDE metadata. @Value can't bind collections/nested structures cleanly and
scatters config across the codebase.
Rule of thumb: A single setting → @Value; a cohesive group of settings → a typed
@ConfigurationProperties class with validation. The typed approach scales far better.
@ConditionalOnProperty registers a bean only when a configuration property has a given
value — a clean on/off switch for features without code branches.
@Configuration
class FeatureConfig {
@Bean
@ConditionalOnProperty(name = "features.cache.enabled", havingValue = "true")
CacheManager cacheManager() { // created only if the flag is "true"
return new CaffeineCacheManager();
}
}
features.cache.enabled=true
You can require a specific havingValue, or use matchIfMissing = true to default to "on"
when the property is absent. This is heavily used in auto-configuration and is the idiomatic
way to ship optional features that users enable via configuration. For arbitrary conditions
beyond properties, drop down to a custom @Conditional(Condition.class).
Rule of thumb: Gate optional beans with @ConditionalOnProperty(... havingValue=...) so
a single config flag turns a feature on or off — no code change, no redeploy logic.
When the built-in conditions aren't enough, implement the Condition interface and
reference it from @Conditional. Its matches method returns whether the bean should be
registered.
class OnLinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
}
@Configuration
class PlatformConfig {
@Bean
@Conditional(OnLinuxCondition.class) // bean exists only on Linux
NativeService nativeService() { return new NativeService(); }
}
ConditionContext exposes the Environment, bean registry, classloader, and resource
loader, so your condition can inspect properties, classpath, existing beans, or anything
else. All of Spring Boot's @ConditionalOnX annotations are just pre-built Condition
implementations on top of this SPI. Keep conditions cheap — they run at startup for every
annotated definition.
Rule of thumb: For logic the built-in @ConditionalOnX annotations can't express,
implement Condition and wire it via @Conditional — it's the same mechanism Spring Boot
uses internally.
Apply @Scope to a @Component class or a @Bean method to override the default singleton
scope — prototype, request, session, or a custom scope.
@Component
@Scope("prototype") // new instance per request
class Task { }
@Configuration
class Config {
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
RequestContext requestContext() { return new RequestContext(); }
}
Built-in scopes: singleton (default), prototype, and the web scopes request, session,
application, websocket. When injecting a shorter-lived scope into a singleton, set
proxyMode so each access resolves the correct instance. There are convenience meta-
annotations too — @RequestScope, @SessionScope, @ApplicationScope — which bundle the
proxy mode for you.
Rule of thumb: Override scope with @Scope on the class or @Bean method; for web
scopes prefer the @RequestScope/@SessionScope shortcuts, which set the proxy mode
automatically.
@Import pulls additional @Configuration classes (or @Components, or an
ImportSelector/ImportBeanDefinitionRegistrar) into the current context — a way to compose
configuration from modular pieces.
@Configuration class SecurityConfig { /* security beans */ }
@Configuration class WebConfig { /* web beans */ }
@Configuration
@Import({ SecurityConfig.class, WebConfig.class }) // compose modular config
class AppConfig { }
@Import lets you split configuration into focused classes and assemble them explicitly,
rather than relying solely on component scanning. It's also the foundation of custom
@Enable* annotations (like @EnableScheduling): those meta-annotate @Import to register a
feature's beans. For dynamic, condition-driven imports you supply an ImportSelector.
Rule of thumb: Use @Import to compose explicit, modular @Configuration classes and to
build your own @Enable* feature annotations; it's how Spring assembles config beyond plain
scanning.
By default in Spring Boot, defining two beans with the same name is an error — bean definition overriding is disabled, and the context fails to start to prevent accidental shadowing.
@Configuration
class ConfigA { @Bean DataSource dataSource() { return ...; } }
@Configuration
class ConfigB { @Bean DataSource dataSource() { return ...; } }
// ✗ Two beans named "dataSource" → BeanDefinitionOverrideException at startup
# To allow later definitions to override earlier ones (rarely advisable):
spring.main.allow-bean-definition-overriding=true
The strict default catches a real class of bugs where one configuration silently replaced
another's bean. The proper fixes are to rename one bean, use @Primary/@Qualifier if
you actually want two of a type, or @ConditionalOnMissingBean so one only registers when
the other is absent — not to flip the override flag.
Rule of thumb: Same-name beans fail by design — rename, qualify, or use
@ConditionalOnMissingBean rather than enabling allow-bean-definition-overriding.
Spring Expression Language (SpEL) is evaluated inside #{...} and lets you compute
values, reference other beans, and read the environment right in annotations.
@Component
class PricingService {
@Value("#{2 * 1024 * 1024}") // arithmetic → 2097152
private int bufferBytes;
@Value("#{systemProperties['user.region'] ?: 'us'}") // env + elvis default
private String region;
@Value("#{discountConfig.rate}") // read a property off another bean
private double rate;
}
#{...} is SpEL (evaluated as an expression); ${...} is a property placeholder
(looked up in the Environment) — they're different and frequently confused. You can even
nest them: #{'${app.mode}' == 'fast'}. SpEL supports method calls, collection projection,
ternary/elvis operators, and bean references. Keep expressions simple; complex logic belongs
in code, not annotations.
Rule of thumb: #{...} = compute a SpEL expression; ${...} = inject a property. Use
SpEL for small computed defaults and bean references, not for business logic.
Component scanning (@Service, @Repository, …) is concise and automatic but implicit;
explicit @Bean config is verbose but gives you full control and works for classes you
can't annotate. Most apps use both.
// Scanning — great for your own classes, zero boilerplate:
@Service class OrderService { }
// @Bean config — necessary for third-party types or custom construction:
@Configuration
class Config {
@Bean ObjectMapper objectMapper() {
return new ObjectMapper().findAndRegisterModules();
}
}
Scanning shines for the bulk of application classes — annotate and forget. Explicit @Bean
methods are required when: the class is third-party (no annotation possible), construction
needs logic/builders, or you want the wiring centralized and visible in one place. The common
pattern: scan your own components, declare infrastructure/library beans explicitly in a few
@Configuration classes.
Rule of thumb: Scan your own code for brevity; write explicit @Bean methods for
third-party types and anything needing construction logic. Real apps blend the two.
Add @Validated to a @ConfigurationProperties class and standard Jakarta Bean
Validation annotations to its fields — invalid configuration then fails the app at
startup instead of causing obscure runtime errors.
@ConfigurationProperties(prefix = "mail")
@Validated
@Component
class MailProperties {
@NotBlank // must be present and non-empty
private String host;
@Min(1) @Max(65535) // valid port range
private int port;
@Email
private String from;
// getters/setters
}
With spring-boot-starter-validation on the classpath, Spring runs the validator when it
binds the properties. A bad value produces a clear BindValidationException at boot naming
the offending property and constraint — much better than discovering a malformed config in
production. This "fail fast on misconfiguration" is a key reason to prefer typed properties
over scattered @Values.
Rule of thumb: Put @Validated + Bean Validation constraints on
@ConfigurationProperties so bad configuration is caught loudly at startup, not silently at
runtime.
A FactoryBean<T> is a bean that produces another object. When something injects the
bean's type, Spring transparently returns the product of getObject(), not the factory
itself — useful for complex or framework-driven construction.
@Component
class ClientFactoryBean implements FactoryBean<ApiClient> {
@Override public ApiClient getObject() {
return ApiClient.builder().retry(3).timeout(5000).build(); // complex build
}
@Override public Class<?> getObjectType() { return ApiClient.class; }
@Override public boolean isSingleton() { return true; }
}
@Service
class Consumer {
Consumer(ApiClient client) { } // gets the ApiClient, NOT the FactoryBean
}
FactoryBean predates @Bean methods and is mostly used inside frameworks (Spring Data
repositories, MyBatis mappers, and JNDI lookups are FactoryBeans under the hood). In
application code a plain @Bean method is simpler and usually preferred. To get the factory
itself rather than its product, prefix the name with & (ctx.getBean("&clientFactoryBean")).
Rule of thumb: In app code, prefer a @Bean method; understand FactoryBean because the
frameworks you use (Spring Data, MyBatis) rely on it to manufacture proxies and clients.
Spring derives a scanned bean's name from its short class name by default. You can override
that strategy globally with a custom BeanNameGenerator passed to @ComponentScan.
class FqnBeanNameGenerator implements BeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition def, BeanDefinitionRegistry reg) {
return def.getBeanClassName(); // use the fully-qualified class name
}
}
@Configuration
@ComponentScan(basePackages = "com.example",
nameGenerator = FqnBeanNameGenerator.class)
class Config { }
Customizing names is rarely needed, but it solves real problems — e.g. two classes with the
same simple name in different packages, which would otherwise collide and fail. Spring Boot
itself uses a FullyQualifiedAnnotationBeanNameGenerator in some setups for exactly this
reason. For one-off naming you'd just pass a value to the stereotype (@Service("name"))
rather than swapping the generator.
Rule of thumb: Set an explicit name on the annotation for one bean; only swap in a custom
BeanNameGenerator when you have systematic naming needs like same-simple-name classes across
packages.
More Dependency Injection interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.