DEV Community

Ankit Verma
Ankit Verma

Posted on

Conditional annotations (@ConditionalOnClass / OnMissingBean ...)

The magic you meet on day one

The first time you use Spring Boot, something strange happens. You add a database library to your project, restart the app, and suddenly there is a fully configured connection pool wired up that you never wrote a line of code for. Remove that library, restart, and the connection pool quietly vanishes — no error, no leftover config, as if it was never there.

That behaviour is not really magic. It is a small, precise decision engine running at startup, and conditional annotations are the switches it flips. This article is about those switches: what they are, why they exist, and the ordering trap that bites almost everyone once.

You meet them the moment you open the source of any Spring Boot starter, or the day you try to replace one of Boot's defaults with your own version and need to understand why your bean wins.

What "creating a bean" actually means here

Two words come up constantly, so let's pin them down before we lean on them.

Spring runs your app inside a container — one big object whose job is to build your other objects and hand them to whoever needs them. Each object the container builds and manages is called a bean. You never call new on these; the container does, and it keeps the single instance around for the whole app.

You tell the container what to build with a configuration class — a plain class whose methods are marked @Bean. Each such method is a recipe: the container calls it once at startup and stores whatever it returns.

@Configuration
class DataSourceConfig {
    @Bean
    DataSource dataSource() {
        return new HikariDataSource(); // the container calls this, keeps the result
    }
}
Enter fullscreen mode Exit fullscreen mode

So far, every @Bean method runs unconditionally. The container sees the recipe, runs it, done. That is exactly the behaviour we now need to make conditional.

Why configuration needs an off switch

Spring Boot ships with hundreds of pre-written configuration classes — this is auto-configuration, Boot's library of "if you seem to want X, here is a sensible X already wired up." There is one for JPA, one for Redis, one for a web server, and so on.

Here is the problem. These configuration classes all sit inside Boot's jars, on your classpath, ready to run. But you do not have every database and every messaging system in one app. If all of them ran, a JPA configuration would try to build a DataSource for a database you never added, and startup would explode.

So every recipe in auto-configuration has to be guarded: "only run me if it actually makes sense." That guard is a conditional annotation.

The base mechanism: @Conditional

Everything starts with one annotation, @Conditional, and one interface, Condition. You hand @Conditional a class, and that class gets a vote on whether the bean is created.

class OnProductionCondition implements Condition {
    @Override
    public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata meta) {
        String env = ctx.getEnvironment().getProperty("app.env");
        return "production".equals(env);
    }
}
Enter fullscreen mode Exit fullscreen mode

The container calls matches before it runs the recipe. Return true and the bean is created; return false and the container skips the recipe entirely, as if it were never written.

@Bean
@Conditional(OnProductionCondition.class)
MetricsPublisher metricsPublisher() {
    return new MetricsPublisher();
}
Enter fullscreen mode Exit fullscreen mode

Read that as: "build a MetricsPublisher, but only if OnProductionCondition says yes." Now the recipe has an off switch.

Writing a Condition class for every guard would be tedious, though. So Boot ships a set of ready-made ones, each wrapped in its own friendly annotation. They are the ones you actually use day to day. Let's build them up one at a time.

@ConditionalOnClass — is the type even here?

The first question auto-configuration must ask is: is the library I depend on actually on the classpath? Your classpath is simply the set of classes available to your running app — everything your dependencies dragged in.

@ConditionalOnClass matches only when a named class can be found there.

@Configuration
@ConditionalOnClass(HikariDataSource.class)
class HikariConfig {
    @Bean
    DataSource dataSource() {
        return new HikariDataSource();
    }
}
Enter fullscreen mode Exit fullscreen mode

This says: "only consider building a Hikari DataSource if the HikariDataSource class is present." Add the Hikari dependency and the guard opens. Leave it out and the whole configuration class is skipped — which is exactly why the connection pool from the opening story appears and disappears with a dependency.

This is the annotation that makes auto-configuration safe to ship "always on." Boot can include a Redis configuration in every app because @ConditionalOnClass keeps it dormant until you actually add Redis.

@ConditionalOnMissingBean — the override hook

Now the most important one, and the one that quietly powers Boot's whole "sensible defaults you can replace" feel.

@ConditionalOnMissingBean matches only when no bean of that type already exists in the container. It is the polite default: "I'll provide this — unless you already did."

@Bean
@ConditionalOnMissingBean
ObjectMapper objectMapper() {
    return new ObjectMapper(); // Boot's default JSON mapper
}
Enter fullscreen mode Exit fullscreen mode

Picture the two ways this could go without the annotation. Suppose you want your own ObjectMapper with custom settings:

// your own config
@Bean
ObjectMapper objectMapper() {
    ObjectMapper m = new ObjectMapper();
    m.setSerializationInclusion(NON_NULL);
    return m;
}
Enter fullscreen mode Exit fullscreen mode

Without the guard, you would now have two ObjectMapper beans, and the container would fail with a conflict. With @ConditionalOnMissingBean on Boot's version, the container checks first, sees that your ObjectMapper already exists, and skips Boot's recipe. Your bean wins, silently, with no configuration on your part.

That single annotation is why overriding a Boot default usually means "just declare your own bean." The default steps aside the moment you provide a replacement.

@ConditionalOnProperty — flip it from config

Sometimes the switch should be a setting, not a class or a bean. @ConditionalOnProperty reads a value from your configuration and matches on it.

@Bean
@ConditionalOnProperty(name = "feature.audit.enabled", havingValue = "true")
AuditListener auditListener() {
    return new AuditListener();
}
Enter fullscreen mode Exit fullscreen mode

The AuditListener bean is created only when feature.audit.enabled=true appears in your properties. This is how you gate an optional feature behind a flag: ship the code, leave it off, turn it on per environment without touching a line of Java.

A small but common trap lives here. By default, if the property is entirely absent, the condition does not match. If you want the feature on unless someone explicitly turns it off, add matchIfMissing = true, which treats "not set" as a match.

The ordering trap that bites everyone

Here is the gotcha the whole article has been building toward. @ConditionalOnMissingBean and its sibling @ConditionalOnBean ask about the current state of the container — what beans exist so far. And "so far" depends entirely on evaluation order.

Think about what that means. If the container checks @ConditionalOnMissingBean before your own bean has been registered, it sees nothing, decides the bean is missing, and builds Boot's default anyway. Your override loses — not because you did anything wrong, but because the check ran too early.

Boot handles this for its own defaults with one firm rule: auto-configuration always runs last. Your configuration classes are processed first, so by the time an auto-configuration recipe asks "does an ObjectMapper already exist?", yours is already in the container and the answer is a reliable "yes."

The trap appears when you write your own configuration and reach for these bean-based conditions between your own beans:

@Bean
@ConditionalOnMissingBean
CacheManager cacheManager() { ... }

@Bean
CacheManager redisCacheManager() { ... } // defined later in the same class
Enter fullscreen mode Exit fullscreen mode

Whether the first recipe sees the second depends on ordering you do not fully control within a single configuration class — and the result can flip between what looks like identical code. The lesson is narrow and worth remembering. @ConditionalOnClass and @ConditionalOnProperty are safe to reason about, because the classpath and your properties are fixed before startup begins. The bean-based conditions are not, because the thing they inspect is still being built while they run. Keep @ConditionalOnMissingBean for the auto-configuration-style "provide a default" case, where Boot's run-last guarantee makes it dependable, and avoid leaning on it to order your own beans against each other.

Putting it together

With all the pieces in hand, a real auto-configuration class reads like a checklist of guards, and you can now follow every line:

@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(name = "app.datasource.enabled", matchIfMissing = true)
class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    DataSource dataSource() {
        return new HikariDataSource();
    }
}
Enter fullscreen mode Exit fullscreen mode

Walk it top to bottom. Run this only if DataSource is on the classpath. And only if the feature is not switched off. And even then, provide a DataSource only if the developer has not already supplied one. Three independent switches, each answering one question, stacking into a single confident decision.

That stack is the whole trick behind Boot feeling like it "just knows" what you want. Nothing knows anything. Each recipe simply refuses to run until its conditions are met — checking the classpath, your properties, and the beans already in the container, in that dependable order.

The one thing to remember

Conditional annotations turn a @Bean recipe from "always run" into "run only if." @ConditionalOnClass guards on what libraries are present, @ConditionalOnProperty on your settings, and @ConditionalOnMissingBean on what beans already exist — the last being the hook that lets your own beans quietly override Boot's defaults. The single sharp edge is that the bean-based conditions depend on evaluation order, which is why auto-configuration is deliberately run last. Once you see the classpath, the properties, and the container as three things being questioned at startup, the magic stops looking like magic and starts looking like a very careful set of switches.

Top comments (0)