AOT mode 'freezes' bean definitions. Explain precisely what is frozen, what still varies at runtime, and which application designs break under this constraint.
answer
- structure frozen, data not
- conditions/profiles evaluated at build time
- @Value / @ConfigurationProperties still runtime
- no runtime profile switching of wiring
- closed-world enforced early; build/run parity
basics
~20 sAOT freezes the set of bean definitions — every @Conditional, @Profile and @ConditionalOnProperty is evaluated at build time, so the beans that exist are fixed. Injected property values still resolve at runtime. Designs that add/remove beans based on runtime environment break.
solid answer
~40 sAOT processing runs the context far enough to compute all bean definitions and then generates code for exactly that set. So everything that decides *which beans exist* — `@ComponentScan` results, `@Conditional`, `@Profile`, `@ConditionalOnClass/Property/Bean`, `@Import` selectors — is evaluated at build time and frozen. What is *not* frozen: values injected via `@Value`/`@ConfigurationProperties`, and normal runtime state; those still resolve from the runtime `Environment`. Consequently, any design that changes bean composition based on the runtime environment breaks: flipping a Spring profile at launch, toggling a `@ConditionalOnProperty` bean via a runtime property, or auto-configuration whose applicability depends on runtime classpath differences. The fix is to run AOT processing with the same profiles/properties/classpath you will run in production, so the frozen shape matches reality. This mirrors the closed-world model native images require, just enforced earlier.
code
java · 23 lines// FROZEN at build time — existence decided by processAot's active profile:
@Service
@Profile("prod")
class RealPaymentGateway implements PaymentGateway { }
@Service
@Profile("!prod")
class FakePaymentGateway implements PaymentGateway { }
// If processAot ran WITHOUT prod, only FakePaymentGateway is generated;
// launching with -Dspring.profiles.active=prod will NOT swap it in.
// STILL RUNTIME — gate BEHAVIOR (not existence) with an injected value,
// which AOT does not freeze:
@Service
class FeatureAwareService {
private final boolean betaEnabled;
FeatureAwareService(@Value("${feature.beta.enabled:false}") boolean betaEnabled) {
this.betaEnabled = betaEnabled; // resolved from runtime Environment
}
void handle() {
if (betaEnabled) { /* ... */ } // safe under AOT: same bean, runtime branch
}
}go deeper
Know bean definitions are fixed at build time and profiles are decided then.
State that conditions/profiles are evaluated at build time while property values still resolve at runtime.
Enumerate the concrete designs that break and the build/run parity remedy, distinguishing structure from data.
Treat AOT artifacts as deployment-shape-specialized, drive per-environment builds or refactor toward value-only environment differences, and gate on AOT tests.
## What 'frozen' means mechanically During AOT processing Spring performs a **special refresh**: it invokes `BeanFactoryPostProcessor`s (notably `ConfigurationClassPostProcessor`, which does component scanning and `@Bean` parsing) so the `BeanDefinitionRegistry` is fully populated — but it stops before instantiating your singleton beans. It then serializes that exact registry into generated Java. The generated `ApplicationContextInitializer` reproduces those definitions verbatim at runtime. Because of this, **anything that influences the contents of the registry is a build-time decision**: - `@ComponentScan` — the scan runs at build time; the discovered components are baked in. - `@Conditional` and all Spring Boot derivatives (`@ConditionalOnProperty`, `@ConditionalOnClass`, `@ConditionalOnMissingBean`, `@ConditionalOnBean`) — evaluated once, at build time. - `@Profile` — the *active profiles during AOT processing* select which profiled beans get generated. - `@Import`, `ImportSelector`, `ImportBeanDefinitionRegistrar`, auto-configuration imports — all resolved at build time. ## What is NOT frozen A frequent point of confusion. AOT freezes the **structure** (which beans, their dependencies, scopes), not the **data**: - `@Value("${...}")` placeholders resolve from the runtime `Environment` on startup. - `@ConfigurationProperties` beans are still bound from runtime config sources. - `Environment`, property files, env vars, command-line args are read normally at runtime. So you can still change *config values* per environment; you just cannot change *which beans exist*. ## Designs that break 1. **Runtime profile switching to change wiring.** Launching the AOT jar with a different `spring.profiles.active` than was used at processing time will NOT add/remove profiled beans. The bean set is whatever processing baked in. 2. **Feature-flag beans via `@ConditionalOnProperty` toggled at runtime.** If the property differs between build and run, the wrong set is baked. (You can still gate *behavior* at runtime with an injected boolean value — just not the bean's existence.) 3. **Classpath-dependent auto-config differing between build and run.** `@ConditionalOnClass` is decided at build time; a runtime classpath that differs produces a mismatched context. 4. **Programmatic/dynamic bean registration that depends on runtime inputs** (e.g. registering beans in a `BeanDefinitionRegistryPostProcessor` based on values only known at runtime) — the AOT snapshot captures only what was knowable at build time. 5. **`@ConditionalOnMissingBean` ordering surprises** if the build-time and runtime bean graphs diverge. ## The remedy: build/run parity Run AOT processing with a configuration that matches production: same active profiles, same relevant properties, same classpath. Treat the AOT-processed artifact as *specialized for one deployment shape*. If you deploy the same jar to multiple environments that need different bean sets, JVM AOT mode may not fit — or you produce per-environment AOT builds. ## Why it is this way The frozen-bean model is the same **closed-world assumption** native images require: the whole point of AOT is to eliminate runtime discovery. JVM AOT mode simply enforces that model early, which is valuable because it surfaces closed-world problems on a debuggable JVM before you ever attempt native compilation. ## Testing for it Use `processTestAot` to run your test suite against the AOT context; tests that assume runtime profile switching will fail there, flagging incompatible designs before production.
- A teammate wants one AOT jar deployed to dev, staging, and prod with different profiles selected at launch. Advise them.That will not work if the profiles change the bean set, because AOT freezes bean existence at processing time. Either run AOT processing per environment (one artifact per shape) or refactor so environments differ only in @Value/@ConfigurationProperties data, not in which beans exist.
- Can you still read environment variables and application.yml values in AOT mode?Yes. AOT freezes bean definitions/conditions, not property resolution. The Environment, property sources, @Value, and @ConfigurationProperties all work normally at runtime.
- How would you catch an AOT-incompatible design before production?Run processTestAot / process-test-aot and execute the suite in AOT mode; tests relying on runtime profile switching or dynamic bean registration will fail there.
saying these in an interview costs you the question
- Claiming @Value values are baked into the AOT artifact
- Believing you can switch profiles at launch to re-wire an AOT jar
- Thinking AOT freezes everything including runtime state
- Assuming one AOT jar can serve environments needing different bean sets