Conditional Annotations

The Common Conditions

AnnotationBean is created when...
@ConditionalOnClass(X.class)class X is on the classpath
@ConditionalOnMissingClass("x.Y")class is NOT on the classpath
@ConditionalOnBean(X.class)a bean of type X already exists
@ConditionalOnMissingBeanno bean of this type exists yet (lets users override)
@ConditionalOnProperty(name="x", havingValue="true")a property has a given value
@ConditionalOnWebApplicationthe app is a web application
@Profile("prod")the profile is active

Choosing a Payment Gateway by Property

public interface PaymentGateway {
    String charge(String orderId, BigDecimal amount);
}

@Configuration
public class PaymentConfig {

    @Bean
    @ConditionalOnProperty(name = "shopease.payment.provider", havingValue = "razorpay")
    PaymentGateway razorpayGateway() { return new RazorpayGateway(); }

    @Bean
    @ConditionalOnProperty(name = "shopease.payment.provider", havingValue = "stripe")
    PaymentGateway stripeGateway() { return new StripeGateway(); }

    @Bean
    @ConditionalOnProperty(name = "shopease.payment.provider", havingValue = "fake", matchIfMissing = true)
    PaymentGateway fakeGateway() { return new FakeGateway(); }   // local development
}

Note: @ConditionalOnMissingBean only sees beans registered before it, so it is reliable in auto-configuration classes (which run after your own configuration) but risky between beans of the same @Configuration class. Property-based conditions, as above, are safer there.

How Conditions Are Evaluated

Example

A plain-Java model: each bean definition carries a condition that is checked before the bean is created:

import java.util.*;
import java.util.function.*;

public class Main {
    public static void main(String[] args) {
        Map<String, String> props = Map.of("shopease.payment.provider", "razorpay");
        Set<String> classpath = Set.of("com.zaxxer.hikari.HikariDataSource");

        List<Definition> defs = List.of(
            new Definition("hikariDataSource", ctx -> classpath.contains("com.zaxxer.hikari.HikariDataSource")),
            new Definition("mongoClient", ctx -> classpath.contains("com.mongodb.client.MongoClient")),
            new Definition("razorpayGateway", ctx -> "razorpay".equals(props.get("shopease.payment.provider"))),
            new Definition("stripeGateway", ctx -> "stripe".equals(props.get("shopease.payment.provider"))),
            new Definition("fakeGateway", ctx -> !ctx.contains("razorpayGateway") && !ctx.contains("stripeGateway"))
        );

        Set<String> context = new LinkedHashSet<>();
        for (Definition d : defs) {
            boolean ok = d.condition().test(context);
            System.out.printf("%-17s %s%n", d.name(), ok ? "CREATED" : "skipped");
            if (ok) context.add(d.name());
        }
        System.out.println("Beans in context: " + context);
    }
}

record Definition(String name, Predicate<Set<String>> condition) {}

Output

hikariDataSource  CREATED
mongoClient       skipped
razorpayGateway   CREATED
stripeGateway     skipped
fakeGateway       skipped
Beans in context: [hikariDataSource, razorpayGateway]

Practice

Exercise: Create a SmsSender bean only when shopease.sms.enabled=true, treating a missing property as false.

Show answer
@Bean
@ConditionalOnProperty(name = "shopease.sms.enabled", havingValue = "true", matchIfMissing = false)
SmsSender smsSender() {
    return new Msg91SmsSender();
}