As an architect adopting native image, what are the failure modes of AOT-generated bean definitions and how do you keep JVM and native behavior in parity?
answer
- AOT = build-time snapshot of the bean factory
- conditions/profiles frozen at processAot
- dynamic runtime registration not captured
- un-hinted reflection → native runtime errors
- parity: test with spring.aot.enabled=true + native smoke test
basics
~20 sThe bean graph is frozen at build time, so runtime-only conditions, dynamic bean registration, and un-hinted reflection break. Mitigate by evaluating profiles at AOT time, providing RuntimeHints/AOT processors, and testing with spring.aot.enabled=true plus native tests.
solid answer
~40 sAOT-generated `*__BeanDefinitions` capture a **snapshot** of the bean factory taken at build time. Failure modes: (1) `@Conditional`/profile decisions are made during `processAot`, so anything depending on runtime environment differs — you must pass the intended profiles to the AOT task. (2) Beans registered dynamically at runtime (e.g., via `BeanDefinitionRegistryPostProcessor` reacting to runtime state, or programmatic `registerBean`) won't be in the generated code. (3) Reflection/resources/proxies not covered by instance suppliers need `RuntimeHints` (via `RuntimeHintsRegistrar` or `@ImportRuntimeHints`), else `ClassNotFoundException`/missing-metadata at native runtime. Parity strategy: run the JVM test suite with `spring.aot.enabled=true` to exercise the generated context early, keep `@Conditional` logic build-time-deterministic, add `BeanRegistrationAotProcessor`s + hints for exotic beans, and run a native smoke/integration test (GraalVM reachability + Spring's native test support) in CI before release.
code
java · 20 lines// Ship hints with the code that needs reflection AOT can't infer:
class MyRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection().registerType(MyDto.class,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.DECLARED_FIELDS);
hints.resources().registerPattern("data/*.json");
}
}
@Configuration
@ImportRuntimeHints(MyRuntimeHints.class)
class MyConfig { /* ... */ }
// Parity test on the JVM (fast) — exercises generated context:
// run with -Dspring.aot.enabled=true
// Hint assertion in a unit test:
// assertThat(RuntimeHintsPredicates.reflection()
// .onType(MyDto.class)).accepts(hints);go deeper
Awareness that native freezes the bean set at build time and needs hints for reflection.
Explain that conditions run at build time and that RuntimeHints are needed for remaining reflection.
Enumerate the failure modes and mitigations, and know the spring.aot.enabled=true fast-parity check.
Own the parity/testing strategy, decide when native is worth the closed-world constraints, and set org-wide conventions for shipping hints + AOT processors with libraries.
## What 'generated bean definitions' fundamentally imply The AOT engine **executes the early phases of context refresh at build time** (creating the `BeanFactory`, applying `BeanFactoryPostProcessor`s, resolving conditions), then serializes the resulting bean graph into Java source. That snapshot is the source of every native-mode failure class: whatever was true at **build time** is what native runs with. ## Failure modes **1. Conditions & profiles evaluated at build time.** `@ConditionalOnProperty`, `@Profile`, `@ConditionalOnClass`, etc. are resolved during `processAot`. If production activates a profile only at runtime, the corresponding beans may never be generated. Mitigation: pass `-Dspring.profiles.active=...` (or Boot's AOT profile configuration) to the AOT task so the build sees the same view; keep conditions deterministic and environment-independent where possible. Note some `@ConditionalOnProperty` can still be re-checked at runtime, but topology-affecting conditions are effectively frozen. **2. Dynamic/programmatic registration.** Beans added by a `BeanDefinitionRegistryPostProcessor` that inspects runtime state, or via `GenericApplicationContext.registerBean(...)` after refresh, aren't captured. `BeanFactoryPostProcessor`s that run at build time ARE reflected (their output is baked in), but ones that need runtime data aren't. Mitigation: make registration static/deterministic, or provide a `BeanRegistrationAotProcessor`/`BeanFactoryInitializationAotProcessor` to emit the needed code + hints. **3. Un-hinted reflection/resources/serialization/proxies.** Instance suppliers remove instantiation reflection, but field injection, Jackson, JPA, `@Value` SpEL, JDK dynamic proxies for interfaces, resource loading (`classpath:` patterns), and `Class.forName` still need metadata in the closed world. Missing hints surface as `ClassNotFoundException`, `MissingReflectionRegistrationError`, empty resource scans, or proxy creation failures at native runtime. Mitigation: implement `RuntimeHintsRegistrar` and wire via `@ImportRuntimeHints`, or contribute hints from an AOT processor; rely on Spring's built-in hints for supported starters; test reachability. **4. Silent semantic drift.** Because AOT changes `@Configuration` handling (no CGLIB) and freezes ordering, subtle differences (bean init order, lazy vs eager) can appear. Mitigation: parity testing. ## Parity strategy (the key architectural discipline) - **Run JVM tests in AOT mode.** Setting `spring.aot.enabled=true` (or Boot's test AOT support) makes the JVM use the generated initializer, so you catch missing hints and frozen-condition issues **without** a slow native compile. This is the highest-ROI check. - **Native smoke tests in CI.** Compile a native image and run integration/reachability tests before release; use Spring's `RuntimeHintsPredicates` in unit tests to assert specific hints are registered. - **Keep conditions build-time-deterministic** and document which profiles the AOT build assumes. - **Package hints with the code that needs them** (library authors ship `RuntimeHints` + AOT processors via `aot.factories`), so consumers get native support for free. - **Treat generated sources as diagnostics:** read `build/generated/aotSources/.../*__BeanDefinitions.java` when a bean is unexpectedly missing or built differently. ## When to adopt Use native/AOT when startup time and memory footprint dominate (serverless, scale-to-zero, CLIs). Accept the constraints: a frozen, statically analyzable bean graph, longer build times, and the discipline of hints. If the app leans heavily on runtime dynamism, weigh whether the closed-world cost is worth it.
- How can you validate AOT/native behavior without paying for a full native compile every time?Run the JVM test suite with `spring.aot.enabled=true` so the generated `ApplicationContextInitializer` and bean definitions are exercised; assert specific hints with `RuntimeHintsPredicates`. Reserve full native compilation + reachability tests for a CI gate before release.
- A bean present on the JVM is missing in native — where do you look first?Check whether a `@Conditional`/profile decision differed at build time (pass the right profiles to `processAot`), whether it's registered dynamically at runtime (not captured), and inspect the generated `*__BeanDefinitions` sources under build/generated to confirm it was never emitted.
- What's the difference between what an instance supplier removes and what RuntimeHints cover?Instance suppliers remove reflective instantiation (constructor/factory-method calls). RuntimeHints cover everything else the closed world still needs reachable: field/method reflection for injection, serialization, resources, JDK proxies, and dynamic class loading.
saying these in an interview costs you the question
- Assuming @Conditional/@Profile are evaluated at runtime under AOT
- Expecting beans registered dynamically at runtime to appear in native
- Thinking instance suppliers make RuntimeHints unnecessary
- Skipping AOT-mode JVM tests and only discovering issues after a native build
- Believing generated bean definitions can be tweaked to add missing runtime beans by hand