What runtime dynamism does AOT give up, and what must be decided at build time?
answer
- bean graph frozen at build time
- conditions & profiles evaluated once
- @ConditionalOnProperty baked in
- values still runtime, beans not
- missing RuntimeHints fail only in native
basics
~20 sAOT freezes the bean graph at build time. Component scanning, @Conditional evaluation, and active profiles are all resolved during process-aot, so you can't add or switch beans by profile or condition at runtime. Missing reflection needs explicit RuntimeHints.
solid answer
~40 sAOT trades runtime flexibility for speed. Because `ApplicationContextAotGenerator` bakes the bean factory at build time, several things become **fixed**: the set of bean definitions, the results of all `@Conditional`/auto-configuration evaluations, and the **active profiles** (evaluated when process-aot runs, not from runtime env). You therefore can't rely on switching profile-scoped beans at runtime, adding definitions dynamically, or having conditions re-evaluate against the runtime environment. Property *values* can still be injected at runtime, but which beans exist cannot change. Reflection, resource loading, and proxies that Spring can't infer must be declared via `RuntimeHints` (through `RuntimeHintsRegistrar`/`@ImportRuntimeHints` or `@RegisterReflectionForBinding`), or they fail — often only visible in the native image. This is why side-effect-free, statically analyzable configuration is essential for AOT-friendly apps.
go deeper
Know the bean set is fixed at build time and reflection may need hints.
List what's frozen (definitions, conditions, profiles) vs. what stays runtime (values).
Explain the @ConditionalOnProperty/profile pitfalls and the JVM-passes/native-fails hint trap.
Judge AOT suitability per workload and set org policy: profile-per-image, hint coverage as done-criteria, side-effect-free config.
## The core tradeoff Spring's normal runtime is **dynamic**: it discovers beans by scanning, decides which to create by evaluating `@Conditional`/`@Profile` against the *runtime* environment, and uses reflection liberally. AOT replaces that with **pre-computed, generated code**, so the flexibility that depends on runtime discovery is lost. ## What is frozen at build time 1. **Bean definitions.** The exact set of beans is captured during `refreshForAotProcessing`. You cannot add/remove definitions at runtime beyond what was generated. 2. **Conditions & auto-configuration.** Every `@ConditionalOnClass`, `@ConditionalOnProperty`, `@ConditionalOnMissingBean`, etc. is evaluated **once, at build time**. If a condition depended on a property, the property value **at build time** decides the outcome. (Note: `@ConditionalOnProperty` outcomes are therefore fixed — a frequent surprise.) 3. **Active profiles.** Profiles are resolved when `process-aot` runs. Beans guarded by `@Profile` are included/excluded based on the *build-time* active profiles, set via `spring.profiles.active` during the goal. Switching profiles at runtime **won't** re-introduce excluded beans. 4. **Proxy classes / reflection surface.** What can be reflected on is limited to what's declared in `RuntimeHints`. ## What still works at runtime - **Property/value injection**: `@Value`, `Environment`, `@ConfigurationProperties` still bind real values from the runtime environment — the *beans* are fixed, but their *configured values* are not (as long as binding was hinted). - **Ordinary business logic, HTTP, DB access** — unchanged. ## RuntimeHints and its failure mode Anything requiring reflection Spring can't statically infer — JSON (de)serialization of DTOs, JPA entities, dynamic proxies, loading a resource by name — needs a **hint**: - `RuntimeHintsRegistrar` + `@ImportRuntimeHints` - `@RegisterReflectionForBinding` for serialization types - `@ReflectiveScan` / `@Reflective` for reflective methods Missing hints usually **compile fine and even run on the JVM**, then fail with `ClassNotFoundException`/`NoSuchMethodException`/serialization errors **only inside the native image** — the classic AOT trap. Testing on the JVM with `spring.aot.enabled=true` plus the GraalVM reachability metadata repository mitigates this. ## Design implications - Keep configuration **statically analyzable**: avoid deciding bean existence via runtime-only signals. - Keep context init **side-effect-free** (process-aot actually refreshes the context). - Pin profiles explicitly in the build. - Treat hint coverage as part of the definition of done for AOT/native. ## When to accept the constraints AOT/native suits **fixed-shape** deployments (serverless, CLI, scale-to-zero) where startup/footprint matter more than runtime reconfiguration. Highly plugin-driven apps that reshape their bean graph at runtime are a poor fit.
- Can @ConfigurationProperties still read runtime values under AOT?Yes — value binding happens at runtime, so property values are read from the live environment. What's fixed is the set of beans and any @Conditional decisions, not the injected values (assuming binding types are hinted).
- Why do missing reflection hints often pass on the JVM but fail natively?The JVM can still reflect at runtime, so it papers over missing hints. GraalVM's closed-world native image only allows reflection that was declared as a RuntimeHint, so the gap surfaces as a runtime failure only there.
- How would you handle a bean that must differ per environment under AOT?Decide it at build time via profiles pinned during process-aot (separate images per profile), or make it a single bean whose *behavior* is driven by runtime-injected properties rather than by bean existence.
saying these in an interview costs you the question
- Claiming you can switch Spring profiles at runtime under AOT to change which beans exist.
- Believing @ConditionalOnProperty is re-evaluated at runtime in an AOT/native app.
- Assuming a JVM run with AOT disabled proves native correctness (missing hints won't show).
- Thinking AOT freezes property values too (it freezes bean definitions, not injected values).