skip to content

AOT Mode on the JVM

Enabling AOT mode on an ordinary JVM runs the generated initializer for a meaningful startup win without native compilation, under the same frozen-bean constraints. A good middle answer when native is too heavy a commitment.

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

explore

questions

5

AOT mode 'freezes' bean definitions. Explain precisely what is frozen, what still varies at runtime, and which application designs break under this constraint.

level: seniorimportance: must knowfreq 34%

answer

  1. structure frozen, data not
  2. conditions/profiles evaluated at build time
  3. @Value / @ConfigurationProperties still runtime
  4. no runtime profile switching of wiring
  5. closed-world enforced early; build/run parity

basics

~20 s

AOT 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 s

AOT 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
java
// 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

for a junior

Know bean definitions are fixed at build time and profiles are decided then.

for a middle

State that conditions/profiles are evaluated at build time while property values still resolve at runtime.

for a senior

Enumerate the concrete designs that break and the build/run parity remedy, distinguishing structure from data.

for a principal

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

context

open as a page

What is Spring's AOT mode on the JVM, and what does the spring.aot.enabled property do?

level: juniorimportance: should knowfreq 22%

basics

~20 s

AOT mode runs code Spring generated at build time — an ApplicationContextInitializer with pre-computed bean definitions — instead of classpath scanning and reflection at startup. Setting spring.aot.enabled=true enables it, giving faster startup on an ordinary JVM.

open as a page

How do you produce and run an AOT-optimized Spring Boot application on a plain JVM, and what are the two distinct phases involved?

level: middleimportance: should knowfreq 28%

basics

~10 s

Two phases. Build phase: run AOT processing (Gradle processAot / Maven process-aot) to generate the context code. Run phase: start the jar on a normal JVM with -Dspring.aot.enabled=true so Spring uses the generated ApplicationContextInitializer.

open as a page

During AOT processing, how far does Spring take the ApplicationContext, and what code runs at build time versus at runtime under spring.aot.enabled?

level: seniorimportance: should knowfreq 24%

basics

~20 s

At build time AOT refreshes the context to obtain all bean definitions — running BeanFactoryPostProcessors but not instantiating your singletons — then generates code. At runtime the generated ApplicationContextInitializer registers those definitions and normal bean instantiation and lifecycle proceed.

open as a page

As a tech lead, when would you adopt JVM AOT mode (spring.aot.enabled) instead of leaving startup unoptimized or going fully native, and what trade-offs govern the decision?

level: principalimportance: should knowfreq 18%

basics

~20 s

Use JVM AOT mode when you want faster startup and lower memory without GraalVM's cost and closed-world debugging pain. It suits scale-to-zero or many-instance JVM deployments, provided a fixed bean shape per build is acceptable. Skip it if you rely on runtime-variable wiring.

open as a page