Spring Configuration & Profiles

Conditional Beans (@Conditional)

18 min Lesson 8 of 13

Conditional Beans (@Conditional)

Spring's IoC container is remarkably flexible: you can tell it to register a bean only when a specific condition is met at startup — a property value, an environment profile, the presence or absence of another class on the classpath, and much more. This is the mechanism behind Spring Boot's famous auto-configuration, and understanding it lets you write configuration classes that adapt intelligently to different environments without a single if statement in your business code.

The Core Abstraction: Condition and @Conditional

Every conditional bean check is built on one interface: org.springframework.context.annotation.Condition. It has a single method:

@FunctionalInterface public interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }

ConditionContext gives you access to the BeanDefinitionRegistry, the ConfigurableListableBeanFactory, the Environment, the ResourceLoader, and the ClassLoader. That means a custom condition can inspect anything Spring knows about at startup time.

You attach a condition to any @Bean method or @Configuration class using @Conditional(YourCondition.class). If matches() returns false, that bean definition is silently skipped — no error, no placeholder.

Evaluation happens before instantiation. Conditions are checked during the bean-definition phase, not when the bean is first requested. This means a false condition removes the bean from the registry entirely — no proxy, no lazy stub.

Built-in Conditions in Spring Boot

Spring Boot ships a rich set of ready-made conditions in the org.springframework.boot.autoconfigure.condition package. These are the building blocks of every auto-configuration starter:

  • @ConditionalOnProperty — registers the bean only when a property key has a specific value.
  • @ConditionalOnClass / @ConditionalOnMissingClass — based on a class being present or absent on the classpath.
  • @ConditionalOnBean / @ConditionalOnMissingBean — based on whether another bean is (or is not) already registered.
  • @ConditionalOnExpression — evaluated against a SpEL expression.
  • @ConditionalOnWebApplication / @ConditionalOnNotWebApplication — based on the application type.
  • @ConditionalOnCloudPlatform — matches a specific deployment platform such as Kubernetes or Cloud Foundry.

Practical Example: Feature-Flag Bean

Suppose a notification service should send real emails in production but write to the console locally. Use @ConditionalOnProperty to switch implementations based on a flag:

# application.properties notifications.email.enabled=true
package com.example.notifications; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class NotificationConfig { @Bean @ConditionalOnProperty(name = "notifications.email.enabled", havingValue = "true") public NotificationService smtpNotificationService() { return new SmtpNotificationService(); } // matchIfMissing = true means this bean registers when the property is absent @Bean @ConditionalOnProperty( name = "notifications.email.enabled", havingValue = "true", matchIfMissing = false ) public NotificationService consoleNotificationService() { return new ConsoleNotificationService(); } }
Use matchIfMissing = true on a fallback bean. Combine it with @ConditionalOnMissingBean to guarantee exactly one implementation is registered even when the property is absent. This pattern appears throughout Spring Boot's auto-configurations.

@ConditionalOnMissingBean: The Extensibility Pattern

The most powerful pattern in Spring Boot auto-configuration is back-off: provide a sensible default but let the application developer override it simply by declaring their own bean of the same type.

package com.example.cache; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CacheAutoConfiguration { // This bean registers ONLY if the application has not already defined a CacheManager @Bean @ConditionalOnMissingBean(CacheManager.class) public CacheManager defaultCacheManager() { return new InMemoryCacheManager(); } }

If the application declares its own CacheManager — say, a Redis-backed one — the auto-configuration backs off silently. No exclusion needed, no property flag. The application's explicit bean takes precedence because Spring processes @Configuration classes in the application before auto-configurations.

Writing a Custom Condition

When no built-in annotation fits, implement Condition directly. Here is a condition that activates a bean only when the application is running on Linux:

package com.example.config; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; public class OnLinuxCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String os = context.getEnvironment().getProperty("os.name", ""); return os.toLowerCase().contains("linux"); } }
package com.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @Configuration public class OsConfig { @Bean @Conditional(OnLinuxCondition.class) public FileWatcherService inotifyFileWatcher() { return new InotifyFileWatcherService(); // Linux inotify-based implementation } }

For reuse across projects, wrap the condition in a composable meta-annotation:

package com.example.config; import org.springframework.context.annotation.Conditional; import java.lang.annotation.*; @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnLinuxCondition.class) public @interface ConditionalOnLinux { }

Now any @Bean method or @Configuration class can simply annotate with @ConditionalOnLinux.

@ConditionalOnExpression for Dynamic Logic

When the condition is best expressed as a Boolean formula over properties, use SpEL:

@Bean @ConditionalOnExpression("'${app.mode}' == 'batch' and ${app.batch.workers:1} > 1") public BatchProcessorPool batchProcessorPool() { return new BatchProcessorPool(); }
Keep conditions simple and fast. Conditions run synchronously during context refresh before any beans are created. Avoid I/O, network calls, or expensive computation inside matches(). A slow condition delays every application startup.

Ordering and Evaluation Order

When a condition checks for the presence of another bean (@ConditionalOnBean), ordering matters. If configuration class B needs to register after class A so it can see A's beans, use @AutoConfigureAfter (in auto-configurations) or @DependsOn (for explicit ordering in application code). Without ordering, the result is non-deterministic.

For regular application @Configuration classes that you control, prefer @ConditionalOnMissingBean over @ConditionalOnBean because you can reason about it without worrying about evaluation order: the condition is essentially "if nobody else registered this type, I will."

Trade-offs and Best Practices

  • Prefer property-based conditions over class-based ones in application code — they are easier to override in tests by setting properties.
  • Avoid nesting too many conditions on one bean. If you need three conditions all satisfied, extract a dedicated @Configuration class and apply the conditions there so they read as a cohesive policy.
  • Document non-obvious conditions with a comment explaining what must be true for the bean to register. Future maintainers (and future you) will thank you.
  • Test conditional beans explicitly. Use ApplicationContextRunner in unit tests to verify which beans register under each combination of conditions without starting a full server.
// Testing with ApplicationContextRunner (no @SpringBootTest needed) import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; class NotificationConfigTest { private final ApplicationContextRunner runner = new ApplicationContextRunner() .withUserConfiguration(NotificationConfig.class); @Test void smtpBeanPresentWhenPropertyTrue() { runner.withPropertyValues("notifications.email.enabled=true") .run(ctx -> assertThat(ctx).hasSingleBean(SmtpNotificationService.class)); } @Test void smtpBeanAbsentWhenPropertyFalse() { runner.withPropertyValues("notifications.email.enabled=false") .run(ctx -> assertThat(ctx).doesNotHaveBean(SmtpNotificationService.class)); } }

Summary

Conditional beans give Spring's container the ability to self-configure based on the runtime environment. The Condition interface is the single extensible hook; all of Spring Boot's auto-configuration annotations — @ConditionalOnProperty, @ConditionalOnMissingBean, @ConditionalOnClass, and the rest — are composable, reusable implementations of that one interface. Write custom conditions when no built-in annotation fits, wrap them in meta-annotations for reuse, and test every conditional path with ApplicationContextRunner to keep your configuration reliable across all environments.