Spring Conditional Bean Configuration: Load Beans Based on Custom Conditions

In a previous post, I made a small demo on how to load Java beans based on properties defined in a configuration file. While this is a very common scenario, sometimes you might need to create beans based on completely custom conditions. Once again, Spring provides an elegant solution for this: the @Conditional annotation. The mechanism is similar to the @ConditionalOnProperty approach, but in this case, we need to define our own custom condition classes.

Let’s imagine that we are building a commerce application and we need to define some discount strategies:

  • on weekends, apply 10% discount
  • apply 50% discount for lucky customers (chance for 1% of customers)

1) For each of the strategies below, we need to define the condition classes: one condition for each strategy. The Condition classes need to implement the Condition interface, which has a single boolean method called matches

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Calendar;

public class WeekendCondition implements Condition {
  @Override
  public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
    Calendar cal = Calendar.getInstance();
    if(cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY || cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY){
      return true;
    }
    return false;
  }
}
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Random;

public class LuckyCustomerCondition implements Condition {
  @Override
  public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
    Random randomGenerator = new Random();
    int number = randomGenerator.nextInt(10);

    return number % 100 == 0;
  }
}

2) Now, we need to define the components that will get loaded based on the conditions. We will need to write three components: one for no discount, one for weekend discount and one for the lucky customer discount. Each of them will implement a common interface called DiscountCalculator.

public interface DiscountCalculator {
  double applyDiscount(double price);
}
import org.springframework.context.annotation.Conditional; 
import org.springframework.stereotype.Component;

/**
 * Standard fares. No discount. Default.
 */

@Component
public class WeekdayDiscountCalculator implements DiscountCalculator {
  @Override
  public double applyDiscount(double price) {
    return price;
  }
}
import org.springframework.context.annotation.Conditional; 
import org.springframework.stereotype.Component;

/**
 * Weekend calculator. Gets loaded only when the WeekendCondition is satisfied.
 */

@Component
@Conditional(WeekendCondition.class)
public class WeekendDiscountCalculator implements DiscountCalculator {
  @Override
  public double applyDiscount(double price) {
    return 0.90 * price;
  }
}
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;

/**
 * Lucky customer calculator. Gets loaded only when the LuckyCustomerCondition is
 * satisfied.
 */

@Component
@Conditional(LuckyCustomerCondition.class)
public class LuckyCustomerDiscountCalculator implements DiscountCalculator {
  @Override
  public double applyDiscount(double price) {
    return 0.5 * price;
  }
}

3) Finally, we need to create the main method for calculating the discount. In this case, I will use a standard RestController. We will load all the DiscountCalculator instances in a list because multiple discounts can be applied (weekend + lucky customer).

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import rc.springboot.services.discount.DiscountCalculator;
import rc.springboot.services.vat.VatCalculator;

import java.util.List;

@RestController
public class PriceController {
  /**
   * We load all available discount calculators to merge discounts. All beans that will
   * satisfy their condition will be added to this list.
   */
  private List<DiscountCalculator> discountCalculators;

  public PriceController(VatCalculator vatCalculator, List<DiscountCalculator> discountCalculators) {
    this.discountCalculators = discountCalculators;
  }

  @GetMapping("/discount/{price}")
  public double applyDiscount(@PathVariable double price) {
    for (DiscountCalculator calculator : this.discountCalculators){
      System.out.println(calculator.getClass().getName());
      price = calculator.applyDiscount(price);
    }
    return price;
  }
}

This is it. We now have a functioning SpringBoot application where we load beans based on custom conditions. The @Conditional annotation is the most generic bean loader. If you want to load components based on more specific conditions (like properties file, Java version, cloud platform, etc.) you can use the more specific @ConditionalOn annotations. Learn how to use the @ConditionalOnProperty annotation to create beans that can be loaded at runtime, based on a property value in the configuration fileYou can learn more about how to use the conditional loaders, in the official Spring documentation.