What does it mean that Spring AOT 'freezes' conditions and the bean set at build time?
answer
- conditions evaluated once at build, code generated
- generated ApplicationContextInitializer just registers, never re-checks
- false @ConditionalOnProperty at build = bean gone forever
- values still runtime-resolved, bean set is not
- native = closed world
basics
~20 sDuring 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 sNormally 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// 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
Know the one-liner: AOT decides which beans exist at build time and generates code for them; runtime doesn't re-check.
Explain which annotations are conditions and that generated code registers a fixed bean set; distinguish frozen bean presence from still-mutable property values.
Discuss the build-time context lifecycle, generated ApplicationContextInitializer + RuntimeHints, and the closed-world native constraint.
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)