skip to content

How and when are @Conditional conditions evaluated, and how would you build a custom condition for the auto-config family?

level: principalimportance: should knowfreq 32%

answer

  1. runs at refresh, before bean instantiation
  2. Condition.matches(ConditionContext, AnnotatedTypeMetadata)
  3. SpringBootCondition -> ConditionOutcome -> evaluation report
  4. ConfigurationCondition: PARSE vs REGISTER_BEAN phase
  5. --debug / actuator conditions endpoint

basics

~10 s

Conditions run at context refresh during bean-definition processing, before beans are instantiated. To build one, implement Spring's Condition (or SpringBootCondition) interface and attach it with @Conditional; auto-config conditions extend SpringBootCondition and report a ConditionOutcome.

solid answer

~40 s

All @Conditional annotations delegate to a Condition whose matches(ConditionContext, AnnotatedTypeMetadata) runs during configuration-class processing at context refresh — before bean instantiation, so conditions can only inspect the Environment, classpath, resource loader, and already-registered bean definitions, not live bean state. Boot's own conditions extend SpringBootCondition, which implements Condition and returns a ConditionOutcome (match + human-readable message) captured in the ConditionEvaluationReport (the --debug 'Positive/Negative matches' output). Some conditions implement ConfigurationCondition to control phase (PARSE_CONFIGURATION vs REGISTER_BEAN) — critical for OnBean/OnMissingBean, which must run in the REGISTER_BEAN phase after other definitions are known. To build your own, implement Condition (or extend SpringBootCondition) and reference it via @Conditional(MyCondition.class), pulling from context.getEnvironment(), getBeanFactory(), getClassLoader(), and getResourceLoader().

code

java · 21 lines
java
// Boot-idiomatic custom condition with a report-friendly outcome
public class OnCloudProviderCondition extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
                                            AnnotatedTypeMetadata metadata) {
        String provider = context.getEnvironment().getProperty("app.cloud");
        ConditionMessage.Builder message = ConditionMessage.forCondition("OnCloudProvider");
        if ("aws".equalsIgnoreCase(provider)) {
            return ConditionOutcome.match(message.found("app.cloud").items(provider));
        }
        return ConditionOutcome.noMatch(message.didNotFind("app.cloud=aws").atAll());
    }
}

@Configuration(proxyBeanMethods = false)
class AwsConfig {
    @Bean
    @Conditional(OnCloudProviderCondition.class)
    S3Client s3Client() { return S3Client.create(); }
}

go deeper

for a junior

Know conditions decide bean registration at startup, not at runtime.

for a middle

Implement a simple Condition via @Conditional and read the Environment from ConditionContext.

for a senior

Explain SpringBootCondition/ConditionOutcome, the evaluation report, and one-shot refresh-time evaluation.

for a principal

Reason about ConfigurationCondition phases, AutoConfigurationImportFilter pre-filtering, and ordering that makes OnBean/OnMissingBean deterministic.

## When conditions run Every `@Conditional`-family annotation (`@ConditionalOnClass`, `OnBean`, `OnProperty`, etc.) is backed by a `Condition`. Conditions are evaluated by the `ConfigurationClassPostProcessor` while it parses `@Configuration` classes and `@Bean` methods — i.e. **at context refresh, during bean-definition registration, before any bean is instantiated**. Consequences: - A condition can read the **`Environment`** (properties/profiles), the **classpath** (`ClassLoader`), the **`ResourceLoader`**, and the **already-registered `BeanDefinition`s** — but **not** the runtime state of instantiated beans. - It's a **one-shot** decision; the resulting bean-definition set is fixed after refresh (no runtime re-evaluation, which is why property changes don't toggle beans). ## The evaluation phase problem — ConfigurationCondition Ordering is subtle. `@ConditionalOnBean`/`@ConditionalOnMissingBean` must run **after** other configuration has contributed its definitions, or they'd see an incomplete picture. Spring solves this with `ConfigurationCondition`, which adds `getConfigurationPhase()`: - **`PARSE_CONFIGURATION`** — evaluated while parsing the config class (e.g. `OnClass`, which doesn't depend on other beans). - **`REGISTER_BEAN`** — evaluated when adding bean definitions, after parsing (e.g. `OnBean`/`OnMissingBean`). This phase choice, plus auto-configuration ordering (`@AutoConfigureBefore/After/Order`) and the fact that auto-config is processed after user config, is what makes bean-presence conditions deterministic. ## SpringBootCondition and ConditionOutcome Boot's conditions don't implement `Condition` directly; they extend **`SpringBootCondition`** (`org.springframework.boot.autoconfigure.condition`). It: - Implements `matches(...)` and delegates to `getMatchOutcome(...)` returning a **`ConditionOutcome`** — a boolean plus a **human-readable message** ('found bean X', 'did not find class Y'). - Feeds the **`ConditionEvaluationReport`**, surfaced by running with `--debug` (or via the `conditions` Actuator endpoint) as **Positive/Negative/Exclusions/Unconditional** matches — the canonical tool for answering 'why did/didn't this auto-config apply?'. ## Building a custom condition ### Simple: implement Condition ```java public class OnLinuxCondition implements Condition { @Override public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) { String os = ctx.getEnvironment().getProperty("os.name", ""); return os.toLowerCase().contains("linux"); } } // usage @Bean @Conditional(OnLinuxCondition.class) InotifyWatcher watcher() { ... } ``` `ConditionContext` exposes `getBeanFactory()`, `getEnvironment()`, `getResourceLoader()`, `getClassLoader()`, and `getRegistry()`. ### Boot-idiomatic: extend SpringBootCondition for report messages ```java public class OnTenantCondition extends SpringBootCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext ctx, AnnotatedTypeMetadata md) { String tenant = ctx.getEnvironment().getProperty("app.tenant"); ConditionMessage.Builder msg = ConditionMessage.forCondition("OnTenant"); return (tenant != null) ? ConditionOutcome.match(msg.found("tenant").items(tenant)) : ConditionOutcome.noMatch(msg.didNotFind("app.tenant property").atAll()); } } ``` Extending `SpringBootCondition` gives you the descriptive `ConditionOutcome` message in the evaluation report — a big debuggability win over a bare `Condition`. ### Meta-annotation packaging Wrap `@Conditional(MyCondition.class)` in a custom annotation (as Boot does for its whole family) for ergonomic reuse. If your condition depends on bean presence, implement `ConfigurationCondition` and return `REGISTER_BEAN`. ## Gotchas at this level - **Don't touch bean instances** in `matches` — they aren't created yet; use definitions/metadata only. - **AutoConfigurationImportSelector** filters auto-config candidates early using `AutoConfigurationImportFilter` (e.g. `OnClassCondition` as a fast pre-filter) so cheap class checks prune the list before full parsing — a performance optimization worth knowing. - **Combining conditions** on one element ANDs them all; there's no built-in OR except via a custom condition or `@ConditionalOnExpression`. ## When to build one Only when the built-ins can't express the signal (OS, region, a computed environment fact). Prefer composing existing conditions first; a custom condition is a maintenance and debuggability cost.

  • Why can't a @Conditional condition inspect the state of an already-created bean?
    Conditions run during bean-definition processing at context refresh, before beans are instantiated. Only definitions, the Environment, classpath, and resources are available. Inspecting live bean state would require ordering guarantees Spring can't provide at that stage.
  • What is ConfigurationCondition and why do OnBean/OnMissingBean need it?
    ConfigurationCondition extends Condition with getConfigurationPhase(). OnBean/OnMissingBean return REGISTER_BEAN so they evaluate after other configuration has registered its definitions; otherwise they'd see an incomplete set of beans and match inconsistently.
  • How do you find out why an auto-config didn't apply?
    Enable the ConditionEvaluationReport by running with --debug (or query the Actuator 'conditions' endpoint). It lists positive/negative matches with the ConditionOutcome messages produced by SpringBootCondition, telling you exactly which condition failed and why.

saying these in an interview costs you the question

  • Saying conditions can read live bean instances during evaluation
  • Believing conditions re-evaluate at runtime when properties change
  • Not knowing OnBean/OnMissingBean run in the REGISTER_BEAN phase
  • Implementing a bare Condition and expecting a descriptive message in the evaluation report

context