Skip to content

Spring Boot · Dependency Injection

Configuring Beans in Spring: @Bean, @Conditional, @Value, and SpEL

5 min read Updated 2026-06-26 Share:

Practice Bean Configuration interview questions

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.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel