skip to content

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?

level: principalimportance: should knowfreq 18%

answer

  1. AOT = build-time snapshot of the bean factory
  2. conditions/profiles frozen at processAot
  3. dynamic runtime registration not captured
  4. un-hinted reflection → native runtime errors
  5. parity: test with spring.aot.enabled=true + native smoke test

basics

~20 s

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

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

for a junior

Awareness that native freezes the bean set at build time and needs hints for reflection.

for a middle

Explain that conditions run at build time and that RuntimeHints are needed for remaining reflection.

for a senior

Enumerate the failure modes and mitigations, and know the spring.aot.enabled=true fast-parity check.

for a principal

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

context