Working with Spring’s @Conditional Annotation for Conditional Bean Registration

Alexander Obregon
11 min readSep 1, 2023
Image Source

Introduction

Spring Framework, which has always been known for its powerful Dependency Injection (DI) capabilities, offers a variety of ways to define and register beans. One such advanced feature is the @Conditional annotation, which allows for conditional bean registration based on certain conditions at runtime. This feature becomes incredibly useful when dealing with different environments, feature flags, or when trying to create highly modular and reusable code.

In this post, we’ll delve into the @Conditional annotation, examine its practical applications, and demonstrate how to use it effectively in a Spring Boot application.

Introduction to Spring’s @Conditional Annotation

In modern software development, adaptability is the name of the game. As developers, we often find ourselves dealing with multiple configurations, environments, and application states. Each of these factors could potentially require different beans, dependencies, or even entirely different configurations within the Spring Framework. Spring provides a powerful solution to manage these complexities through its @Conditional annotation.

The Spring Framework has been a pioneer in the realm of Dependency Injection (DI) and Inversion of Control (IoC). These paradigms allow you to write flexible, testable, and modular code. While Spring’s basic features such as @Component, @Service, @Repository, and @Autowired offer straightforward ways to define and inject beans, real-world scenarios often require more flexibility. That's where advanced features like the @Conditional annotation come into play.

Introduced in Spring 4.0, the @Conditional annotation allows you to register beans conditionally. What does that mean? In essence, it enables you to control whether a particular bean is created or not, based on specific conditions. The Spring Container checks these conditions at runtime and decides whether the annotated bean should be included in the application context.

Versatility and Applicability

One of the standout features of @Conditional is its versatility. The annotation can be applied at various levels:

  • Method Level: When used on a @Bean method, the @Conditional annotation tells Spring to conditionally create the bean defined by that method.
@Bean
@Conditional(SomeCondition.class)
public MyBean myBean() {
return new MyBean();
}
  • Class Level: When applied on a @Configuration class, it conditionally configures all @Bean methods within that class.
@Configuration
@Conditional(SomeCondition.class)
public class AppConfig {
// ... your @Bean definitions here
}
  • Component Level: The annotation can also be used directly on @Component, @Service, and @Repository classes, adding an additional layer of control over the component scanning process.
@Component
@Conditional(SomeCondition.class)
public class MyComponent {
// ... your component code here
}

How It Works Internally

Under the hood, Spring’s @Conditional annotation works by leveraging the Condition interface. This interface must be implemented by a class that contains the conditional logic. When Spring evaluates this logic, it decides whether to register the annotated bean or not.

A typical Condition interface implementation would look something like this:

public class SomeCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Your custom logic to evaluate the condition
return true; // or false, depending on the condition
}
}

By mastering the @Conditional annotation, developers can write highly flexible, dynamic, and environment-sensitive applications without compromising the core benefits of using the Spring Framework.

Use Cases for Conditional Bean Registration

The necessity for conditionally registering beans often arises from the intricate and diverse requirements that modern applications must fulfill. Whether you are dealing with different deployment environments or modular architectures, conditional bean registration serves as a pivotal feature to navigate these complexities. Below, we explore several key scenarios where the @Conditional annotation can be immensely beneficial.

Environment-specific Beans

In an ideal world, an application would behave consistently regardless of its environment. However, in reality, there are always variations between environments such as development, staging, and production. These could be in terms of database configurations, caching strategies, or security protocols.

For example, you may want to use a mock service for local development and the actual service for production:

@Configuration
public class ServiceConfig {

@Bean
@Conditional(DevelopmentCondition.class)
public MyService mockService() {
return new MockMyServiceImpl();
}

@Bean
@Conditional(ProductionCondition.class)
public MyService realService() {
return new RealMyServiceImpl();
}
}

Feature Flags

Feature flags or feature toggles are a powerful technique for altering an application’s behavior without changing its code. They allow you to enable or disable features dynamically, which can be particularly useful during A/B testing, gradual feature rollouts, or emergency rollbacks.

Imagine having an email notification system that is under testing and you want to enable or disable it using a feature flag:

@Bean
@Conditional(EmailNotificationEnabledCondition.class)
public NotificationService emailNotificationService() {
return new EmailNotificationServiceImpl();
}

Modular Architecture

In a microservices architecture or even within a monolithic application that’s modular, you might want to enable or disable entire modules or services based on the deployment configuration or even at runtime.

For instance, if you have a payment module that should only be active when certain conditions are met, you could do something like this:

@Configuration
@Conditional(PaymentModuleEnabledCondition.class)
public class PaymentConfig {
// Define your payment-related beans here
}

Resource Availability

Sometimes you may want to conditionally register a bean based on the availability of a certain resource like a file, a database, or an environment variable. This is useful for fallback mechanisms.

@Bean
@Conditional(DatabaseAvailableCondition.class)
public MyDatabaseService myDatabaseService() {
return new MyDatabaseServiceImpl();
}

Conditional API Endpoints

In some advanced scenarios, you may even want to conditionally expose or hide certain REST API endpoints based on user roles, subscription levels, or some other business logic. This can be achieved through conditional bean registration of controllers.

@RestController
@Conditional(PremiumUserCondition.class)
public class PremiumFeatureController {
// Premium user-specific APIs here
}

Understanding these use cases not only offers you a broader perspective on what can be achieved with conditional bean registration but also empowers you to solve specific requirements in an elegant and maintainable way.

Basic Syntax and How It Works

The basic premise of Spring’s @Conditional annotation is fairly straightforward: conditionally register a bean based on whether a condition is met. But understanding how this mechanism actually works under the hood can help you better leverage its full capabilities. Let's dissect the basic syntax, required interfaces, and internal mechanisms that make this feature so useful and versatile.

Basic Syntax

At its core, the @Conditional annotation accepts a class that implements the Condition interface. This interface defines a single method, matches, which evaluates whether the condition is met or not.

Here’s a basic example, which conditionally creates a MyBean instance based on a simple condition:

@Configuration
public class AppConfig {

@Bean
@Conditional(MySimpleCondition.class)
public MyBean myBean() {
return new MyBean();
}
}

The Condition Interface

The Condition interface is the cornerstone of the @Conditional annotation. It consists of a single method:

public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
  • ConditionContext: Provides context information during the evaluation, including details about the bean factory, environment, and class loader.
  • AnnotatedTypeMetadata: Offers metadata of the annotated element, which can be a class, method, or field.

Implementing a Custom Condition

Implementing a custom condition involves creating a class that implements the Condition interface and overriding the matches method. Let’s create a simple example that registers a bean only if a system property enableMyBean is set to true.

public class MySimpleCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String enabled = System.getProperty("enableMyBean");
return "true".equalsIgnoreCase(enabled);
}
}

Condition Evaluation

The Spring Framework goes through a series of steps to evaluate whether a condition is met:

  1. Parsing Configuration: During this phase, Spring identifies all beans that are candidates for creation and examines any associated conditions.
  2. Condition Evaluation: The matches method of each condition class is invoked. This method contains the logic that determines whether the condition is met.
  3. Bean Registration: If all conditions for a bean are met, it gets registered. Otherwise, it is skipped.
  4. Dependency Resolution: Once all conditions are processed, Spring resolves dependencies and injects them where needed.

Combining Multiple Conditions

You can also combine multiple conditions to form more complex logical structures. Spring Framework allows chaining of conditions using the @Conditional annotation like so:

@Bean
@Conditional({DatabaseAvailableCondition.class, EmailServiceEnabledCondition.class})
public MyService myService() {
return new MyServiceImpl();
}

In this example, MyService will only be registered if both DatabaseAvailableCondition and EmailServiceEnabledCondition evaluate to true.

Programmatic Condition Evaluation

While less common, Spring also offers a way to evaluate conditions programmatically using the ConditionEvaluator interface. This is useful for very dynamic scenarios where conditions might be too complex for the @Conditional annotation alone.

By delving into these facets of the @Conditional annotation, you gain a well-rounded understanding of its inner workings, enabling you to use it effectively in complex projects.

Creating Custom Conditions

The built-in conditions offered by Spring are robust, but there are scenarios where you might require a more tailored approach. In such cases, creating a custom condition can offer you the flexibility to define your own evaluation logic. This section dives into the details of creating custom conditions, how to evaluate them, and integrating them into your application.

Understand the Condition Interface

Before crafting a custom condition, you must have a solid grasp of the Condition interface, which consists of a single method:

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

You implement this method to contain the logic that Spring will evaluate to decide whether to register a bean.

Access to Context and Metadata

The matches method receives two parameters:

  • ConditionContext: Gives access to the application context, environment, class loader, and other beans.
  • AnnotatedTypeMetadata: Offers metadata about the annotated element, be it a class, method, or field.

You can use these parameters to perform dynamic evaluations based on the application’s state.

Step-by-step Custom Condition Creation:

Step 1: Implement the Condition Interface:

First, you’ll need to create a class that implements the Condition interface. For example, let's create a custom condition that will check if a system property my.custom.property is set.

public class CustomPropertyCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("my.custom.property");
}
}

Step 2: Annotate with @Conditional

Once your custom condition is ready, annotate the relevant bean or configuration with @Conditional, passing in your custom condition class.

@Configuration
public class AppConfig {

@Bean
@Conditional(CustomPropertyCondition.class)
public MyBean myBean() {
return new MyBean();
}
}

Combining Custom and Built-in Conditions

You can even combine custom conditions with built-in conditions to form complex logical conditions. For example:

@Bean
@Conditional({CustomPropertyCondition.class, OnPropertyCondition.class})
public MyBean myBean() {
return new MyBean();
}

In this example, MyBean will only be registered if both CustomPropertyCondition and OnPropertyCondition evaluate to true.

Advanced Custom Conditions: Examining AnnotatedTypeMetadata

The AnnotatedTypeMetadata parameter in the matches method enables more advanced scenarios. It allows you to access annotations on the class, method, or field to which @Conditional is applied.

Here’s an example of how this might be used:

public class RoleBasedCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(RoleConditional.class.getName());
String requiredRole = (String) attributes.get("value");

// Your logic to check if the current user has the required role
return checkUserRole(requiredRole);
}

private boolean checkUserRole(String requiredRole) {
// Custom logic to check user role
return true; // Or false, based on your evaluation
}
}

This custom condition utilizes a custom annotation named RoleConditional to perform its evaluation.

Advanced Use Cases

While the @Conditional annotation is a powerful feature for handling many common situations, it becomes truly indispensable when dealing with complex application requirements. In this section, we'll explore some advanced use cases that demonstrate the versatility of conditional bean registration.

Conditional Beans Based on Profiles and Properties

You can combine the @Conditional annotation with Spring profiles and properties for finely grained control over bean registration. For example, you could create a custom condition that only registers a bean when a certain profile is active and a specific property is set.

public class ProfileAndPropertyCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.acceptsProfiles("production") && env.containsProperty("enableSpecialFeature");
}
}

Conditional Scheduling

Spring’s scheduling capabilities, usually applied via annotations like @Scheduled, can also be conditionally enabled or disabled. Imagine a scenario where a scheduled task should only be enabled when the application is running in a specific geographical location.

@Component
@Conditional(GeolocationCondition.class)
public class GeolocationBasedScheduler {

@Scheduled(fixedRate = 5000)
public void executeTaskBasedOnGeolocation() {
// Task execution logic
}
}

Nested Conditional Beans

You can create a hierarchy of conditional beans where the existence of one bean depends on another conditionally created bean. This can be useful for creating multi-layer abstractions where one service might depend on another service, which in turn depends on certain conditions.

@Bean
@Conditional(DatabaseAvailableCondition.class)
public DataSource dataSource() {
// Create DataSource
}

@Bean
@ConditionalOnBean(DataSource.class)
public MyDatabaseService myDatabaseService() {
// Create MyDatabaseService only if DataSource is available
}

Conditional Message Listeners

In a microservices architecture, you might have services that listen to events or messages from a message queue. Sometimes, you might want to conditionally enable or disable specific message listeners.

@Component
@Conditional(MessageQueueEnabledCondition.class)
public class MyMessageListener {

@KafkaListener(topics = "myTopic")
public void listen(String message) {
// Do something
}
}

Dynamic Bean Definitions

In highly dynamic applications, you might need to define beans at runtime based on some complex logic. While it’s advisable to use @Conditional sparingly for such use cases, it can be done using programmatic bean registration.

@Configuration
public class DynamicBeanConfig {

@Bean
@Conditional(DynamicCondition.class)
public MyDynamicBean myDynamicBean() {
return new MyDynamicBean();
}
}

public class DynamicCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Complex logic to evaluate the condition
return true; // or false
}
}

Conditional Security Configurations

Security configurations can be very different depending on whether your application is running internally or exposed to the Internet. You can conditionally load security configurations to suit the deployment environment.

@Configuration
@Conditional(InternalNetworkCondition.class)
public class InternalSecurityConfig extends WebSecurityConfigurerAdapter {
// Configuration for internal network
}

These advanced use cases demonstrate the power and flexibility of the @Conditional annotation, which can adapt to complex scenarios in a robust and maintainable way. Whether dealing with layered dependencies, dynamic environments, or specialized configurations, @Conditional has you covered.

Common Pitfalls

The @Conditional annotation is a potent tool for conditional bean registration, but its misuse can lead to confusing scenarios, inconsistencies, or even application failures. In this section, we will delve into some common pitfalls to watch out for.

Neglecting the Bean Overriding Feature

Spring allows you to define multiple beans of the same type, and by default, the last declared bean will override any previous ones. When using @Conditional, it's crucial to remember that if two conditions can be true simultaneously, you may end up with unexpected bean overriding.

@Bean
@Conditional(OnDevelopmentCondition.class)
public MyService devMyService() {
return new DevMyService();
}

@Bean
@Conditional(OnProductionCondition.class)
public MyService prodMyService() {
return new ProdMyService();
}

If both OnDevelopmentCondition and OnProductionCondition are true (which ideally should never happen but can due to misconfiguration), the latter bean will override the former.

Circular Dependencies

Be cautious when creating conditional beans that depend on each other, as this can lead to circular dependencies. Spring may not be able to resolve such dependencies correctly, causing the application to fail at startup.

@Bean
@Conditional(ConditionA.class)
public ServiceA serviceA(ServiceB serviceB) {
return new ServiceA(serviceB);
}

@Bean
@Conditional(ConditionB.class)
public ServiceB serviceB(ServiceA serviceA) {
return new ServiceB(serviceA);
}

Confusing @ConditionalOnBean and @Conditional

While @ConditionalOnBean might appear similar to @Conditional, they operate at different phases of the application context lifecycle. @ConditionalOnBean checks conditions during bean creation, whereas @Conditional evaluates conditions before the bean definition is registered.

Condition Caching

Spring caches the result of condition evaluations for performance reasons. This means if your condition has side-effects or relies on mutable state, you might see inconsistent behavior.

Mixing Configuration Styles

Using @Conditional within XML-based configuration or with legacy bean factories can result in unexpected behavior, as the annotation is intended primarily for use with Java-based configuration.

Relying on External State

Be cautious when your condition depends on external states like databases or remote services. Such dependencies can make the startup process fragile and slow.

Using @Conditional on @Component

While you can use @Conditional on component classes (@Component, @Service, etc.), doing so might make it hard to predict the behavior, especially when component scanning is involved. It is often better to restrict the use of @Conditional to @Bean methods within @Configuration classes for greater control and clarity.

Overusing @Conditional

While it’s tempting to use @Conditional to manage various scenarios, overuse can lead to a configuration that's hard to understand and maintain. Use it judiciously and always document why a particular conditional configuration is in place.

Conclusion

Spring’s @Conditional annotation provides a powerful yet straightforward way to add conditional logic to your bean registrations. By mastering this feature, you can make your applications more flexible and better suited to different scenarios and environments.

  1. Spring Official Documentation on Conditional Annotation
  2. Java Interface Tutorial
  3. Spring Profiles
Spring Boot icon by Icons8

--

--

Alexander Obregon
Alexander Obregon

Written by Alexander Obregon

Software Engineer, fervent coder & writer. Devoted to learning & assisting others. Connect on LinkedIn: https://www.linkedin.com/in/alexander-obregon-97849b229/