skip to content

Build-Time Frozen Conditions

AOT evaluates conditions, profiles and auto-configuration at build time and freezes the resulting bean set, so switching profiles at runtime no longer changes anything. Interviewers ask what you lose going native, and this rigidity is the biggest item.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

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

open as a page

Why does changing spring.profiles.active at runtime fail to swap beans in an AOT / native Spring Boot app?

level: middleimportance: must knowfreq 60%

basics

~20 s

Because @Profile is a condition. AOT evaluates profiles at build time and bakes in only the beans for the profiles active then. Activating a different profile at runtime cannot add beans that were never generated.

open as a page

Does the frozen-condition behavior only apply to GraalVM native images, or also to JVM apps? How would you reproduce and diagnose it?

level: middleimportance: should knowfreq 35%

basics

~10 s

It also applies on the JVM. Setting spring.aot.enabled=true runs the same generated, frozen-bean initializer. So you can reproduce and debug frozen-condition issues on a normal JVM before ever building a native image.

open as a page

How do dynamic / programmatic bean registration mechanisms behave under Spring AOT, and what breaks?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Programmatic registration (BeanDefinitionRegistryPostProcessor, ImportBeanDefinitionRegistrar, registerBean) runs at build time during AOT and its output is frozen into generated code. Registration that depends on runtime state, or done after startup, is lost in native images.

open as a page

Given build-time frozen conditions, how do you architect a Spring Boot service that still needs environment-specific and runtime-toggleable behavior across native deployments?

level: principalimportance: should knowfreq 30%

basics

~20 s

Move variability from bean presence to bean behavior: always register the beans, decide at runtime via configuration values. Reserve @Profile/@Conditional for build-time-stable choices, and build one native image per environment when bean sets truly differ.

open as a page