AOT and Native Image

Introduction

Spring Boot 3 supports Ahead-of-Time (AOT) processing and GraalVM Native Image to compile apps into small native binaries with fast startup and lower memory—ideal for serverless and dense containers. Trade-offs include longer build times and reflection limits. This chapter explains when native fits, runs a basic native build, and lists compatibility checks.

Prerequisites

JVM Jar vs Native Image

JVM java -jarNative image
StartupSeconds (warm JVM)Milliseconds typical
MemoryHigher baselineLower footprint
Build timeFastSlow (minutes)
Reflection / dynamicFullNeeds reachability hints
Peak throughputOften higher on JVMWorkload dependent

Use native for scale-to-zero, CLI tools, edge—not default for every CRUD API until you measure.

Spring AOT Processing

Spring Boot 3 generates reflection and bean metadata at build time:

bash
mvn -Pnative native:compile

Or with Spring Boot plugin:

bash
mvn -Pnative spring-boot:build-image

Cloud Native Buildpacks produce OCI image with native binary inside—no local GraalVM required if using Paketo builder.

pom.xml profile snippet:

Follow current Spring Boot 3 docs for exact plugin versions aligned to your BOM.

Native-Compatible Dependencies

Prefer:

  • spring-boot-starter-web (Servlet) with supported stack
  • Spring Data JPA with tested drivers—verify native hints

Problematic without extra config:

  • Dynamic proxies everywhere
  • Unsupported libraries using heavy reflection
  • Class.forName runtime loading

Run native:compile and read build-time errors—add reflect-config.json or RuntimeHints registrar when required.

RuntimeHints Example (Boot 3)

java
@Configuration
public class MyRuntimeHints implements RuntimeHintsRegistrar {
 
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        hints.reflection().registerType(MyDto.class, MemberCategory.DECLARED_FIELDS);
    }
}

Register via META-INF/spring/org.springframework.aot.hint.RuntimeHintsRegistrar.

Test Native Locally

After build:

bash
./target/demo-app
curl http://localhost:8080/hello

Observe startup log timestamp vs JVM run—native should start faster.

Docker With Native Image

bash
mvn spring-boot:build-image -Pnative -Dspring-boot.build-image.imageName=demo-native:1.0
docker run -p 8080:8080 demo-native:1.0

Smaller image than full JRE layer—good for Docker deploys.

When Not to Use Native

  • Heavy dynamic scripting, unknown classpath scanning
  • Team lacks GraalVM build pipeline patience
  • App benefits from JVM JIT peak performance on long-running batch

Start with JVM in prod; adopt native for specific services after profiling.

Tip

Validate Before Committing Architecture

Build native in CI weekly even if prod stays JVM—surfaces hint gaps early.

FAQ

native build fails on MyBatis dynamic SQL?

May need reachability metadata or stay on JVM for that service.

Smaller = always cheaper?

Dev time and CI minutes cost money too—weigh TCO.

Spring Boot 2 native?

Use Boot 3 + GraalVM 22+ toolchain.

Native + Spring Native project?

Merged into official Spring Boot 3 native support—ignore old Spring Native docs.

Windows native exe?

GraalVM builds Windows executable—Linux target common in CI/CD.

AOT without native?

spring-aot-maven-plugin processes for faster startup on JVM too—subset of benefits.