skip to content

What does it mean that Spring AOT 'freezes' conditions and the bean set at build time?

level: juniorimportance: must knowfreq 55%

answer

  1. conditions evaluated once at build, code generated
  2. generated ApplicationContextInitializer just registers, never re-checks
  3. false @ConditionalOnProperty at build = bean gone forever
  4. values still runtime-resolved, bean set is not
  5. native = closed world

basics

~20 s

During an AOT build Spring evaluates all @Conditional/@Profile checks and auto-configuration once, decides which beans exist, and generates code registering exactly those beans. Those decisions are fixed; they are not re-checked when the app runs.

solid answer

~40 s

Normally Spring evaluates conditions (@Conditional, @Profile, @ConditionalOnProperty, @ConditionalOnClass, auto-configuration) every time the context starts. With Ahead-Of-Time processing (spring.aot.enabled, and always in a GraalVM native image), Spring Boot runs part of the context lifecycle at build time: it evaluates every condition and auto-configuration, resolves the final set of bean definitions, and emits generated Java code plus reflection/resource hints. At runtime that generated ApplicationContextInitializer just registers the already-decided beans — conditions are not re-run. So the bean graph is 'frozen': what was true at build time is what you get. A @ConditionalOnProperty that was false during the build produces no bean, and no runtime property flip can bring it back.

code

java · 13 lines
java
// Evaluated at BUILD time under AOT, not at runtime.
@Configuration
class FeatureConfig {

    // If 'feature.beta.enabled' is not true during the AOT build,
    // BetaService is never generated. Flipping the property at
    // runtime will NOT create the bean.
    @Bean
    @ConditionalOnProperty(name = "feature.beta.enabled", havingValue = "true")
    BetaService betaService() {
        return new BetaService();
    }
}

go deeper

for a junior

Know the one-liner: AOT decides which beans exist at build time and generates code for them; runtime doesn't re-check.

for a middle

Explain which annotations are conditions and that generated code registers a fixed bean set; distinguish frozen bean presence from still-mutable property values.

for a senior

Discuss the build-time context lifecycle, generated ApplicationContextInitializer + RuntimeHints, and the closed-world native constraint.

for a principal

Frame the design consequence: environment differences must be resolved at build time; toggles become build/image-selection decisions, not runtime reconfiguration.

## The normal (JIT) lifecycle In a standard Spring Boot app the ApplicationContext is built when the JVM starts. During `refresh()` Spring reads bean definitions and evaluates **conditions**: annotations like `@Conditional`, `@Profile`, and the Boot family `@ConditionalOnProperty`, `@ConditionalOnClass`, `@ConditionalOnMissingBean`, plus all of **auto-configuration**. Because this happens at startup, the same jar can behave differently depending on the environment, active profiles, and property files present at that moment. ## What AOT changes **AOT = Ahead-Of-Time processing.** It is turned on by `spring.aot.enabled=true` (the Spring Boot Gradle/Maven `processAot` task/goal sets this) and is *mandatory* for **GraalVM native images**, which use a **closed-world** model where the whole reachable program must be known at build time. During the AOT build Spring Boot actually starts the context far enough to evaluate every condition and auto-configuration, determines the **final set of bean definitions**, and then **generates source code** — an `ApplicationContextInitializer`, `BeanDefinition` builders, and `RuntimeHints` (reflection/resource/proxy metadata). This generated code is compiled into the app. ## Why 'frozen' At runtime the app does **not** re-run the condition evaluation. The generated initializer simply registers the bean definitions that AOT already decided on. Concretely: - A `@ConditionalOnProperty("feature.x.enabled")` that evaluated **false** during the build means the guarded bean is **absent from the generated code**. Setting `feature.x.enabled=true` at runtime does nothing — there is no code to activate. - A `@Profile("cloud")` bean is only baked in if `cloud` was active **during the build**. - Auto-configuration classes excluded at build time (e.g. because a class was not on the classpath, or a bean was already present) are gone for good. What is **not** frozen: ordinary `@Value`/`@ConfigurationProperties` values that a *baked-in* bean reads are still resolved at runtime from the live `Environment`, so you can still change configuration *values*. What you cannot change is which **beans exist**. ## Where it bites - Toggling features via a property at runtime when the toggle is a `@ConditionalOnProperty` — the beans were chosen at build time. - Switching Spring **profiles** at runtime to swap in different beans — profile-gated beans are frozen (see the dedicated profile question). - **Dynamic/programmatic** bean registration (`BeanDefinitionRegistryPostProcessor`, `ImportBeanDefinitionRegistrar`, `GenericApplicationContext.registerBean`) runs at *build* time under AOT; runtime dynamic registration is not part of the frozen graph and, in native images, is effectively unsupported. ## When to care Any app you plan to ship as a native image, or run with `spring.aot.enabled=true` on the JVM, must have its conditions/profiles/feature decisions resolvable at build time. Design toggles as build-time choices (separate images/profiles per environment) rather than expecting one universal jar to reconfigure itself at boot.

  • If bean presence is frozen, can I still change any configuration at runtime in a native image?
    Yes — property *values* read by beans that were baked in (via @Value/@ConfigurationProperties/Environment) are still resolved at runtime. Only the *set of beans* (what @Conditional/@Profile decided) is fixed.
  • How do you turn AOT on for a JVM app to reproduce this behavior without building a native image?
    Set spring.aot.enabled=true (the Spring Boot processAot task generates the sources). It exercises the same frozen-bean initializer, so you can test AOT behavior on the JVM before going native.

saying these in an interview costs you the question

  • Thinking conditions are re-evaluated at every startup even under AOT/native
  • Believing a runtime property flip can re-activate a @ConditionalOnProperty bean
  • Confusing 'bean set frozen' with 'all configuration frozen' — values can still change
  • Assuming AOT only matters for native images (it also runs on the JVM with spring.aot.enabled)

context