GraalVM and Spring Boot – Best Practices for Native Image Spring Apps
Spring Boot has long been the go-to framework for building production-grade Java applications quickly. But one of the biggest criticisms of Java has always been slow startup times and high memory usage. That’s where GraalVM Native Image comes in—allowing developers to compile Java applications into native executables that start up in milliseconds and consume far less memory.
If you’re building cloud-native apps, microservices, or serverless functions, Spring Boot + GraalVM can be a game-changer. But as with any powerful tool, you need to know the best practices to get the most out of it.
Let’s dive in.
Why Use GraalVM Native Image with Spring Boot?
Before talking best practices, it’s worth clarifying the benefits:
- Instant Startup – From seconds down to tens of milliseconds.
- Lower Memory Footprint – Great for cloud deployments and containers.
- Smaller Attack Surface – Only required code is compiled into the image.
- Optimized for Scale – Perfect for microservices that spin up and down frequently.
For example, a simple Spring Boot REST API that takes ~2.5 seconds to start on the JVM may start in just 50 ms as a native image.
Best Practices for Building Native Spring Boot Apps
1. Use the Right Spring Boot Version
- Start with Spring Boot 3+ since it has first-class support for GraalVM native images via Spring AOT (Ahead-of-Time) processing.
- Add the Spring Native plugin:
<plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>0.10.1</version> </plugin>
Or with Gradle:
plugins {
id 'org.graalvm.buildtools.native' version '0.10.1'
}
2. Leverage Spring AOT Processing
Spring AOT transforms your application code and configuration at build time to remove unnecessary reflection and dynamic class loading.
- Enable AOT mode by building with:
./mvnw -Pnative native:compile
- This produces a self-contained native binary ready for deployment.
3. Be Mindful of Reflection & Proxies
Native images don’t handle reflection gracefully by default. If your Spring Boot app uses libraries that rely heavily on reflection (like Hibernate), you’ll need to:
- Add reflection hints:
@ReflectionHint(types = MyEntity.class)
public class MyHints implements NativeConfiguration {}
- Or register configuration in
META-INF/native-image/JSON files.
👉 Best practice: use Spring’s AOT-generated hints whenever possible to minimize manual work.
4. Optimize Dependencies
Not all dependencies are native-image friendly. For example:
- Avoid libraries that dynamically generate classes at runtime.
- Prefer Jakarta Persistence (via Hibernate 6.x) which has native support.
- Use Spring Boot’s dependency compatibility list as a reference.
5. Tune Memory and Performance
Native images are smaller but not always faster at raw throughput than the JVM.
- Use native images for startup-sensitive apps (microservices, CLI tools, serverless).
- Stick with the JVM for long-running, throughput-heavy apps (batch jobs, big data pipelines).
Example:
- Spring Boot API as native image: cold-start in 60 ms.
- Same API on JVM: cold-start in 2.4 seconds, but higher throughput after warm-up.
6. Containerize Your Native Images
If deploying to Kubernetes or Docker:
- Use a distroless base image for minimal footprint:
FROM gcr.io/distroless/base COPY build/myapp / CMD ["/myapp"]
- Keep your images small (<50MB) compared to JVM-based images (~300MB).
7. Embrace Observability
Native images change runtime behavior, so ensure monitoring still works:
- Add Micrometer with Prometheus or Grafana.
- Use Spring Boot Actuator endpoints (fully supported in native mode).
Example: REST API with Native Image
A simple Spring Boot controller:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello from GraalVM Native!";
}
}
Build and run as a native image:
./mvnw -Pnative native:compile ./target/hello
Result:
Startup time: 0.045s Memory: ~30MB RSS
JVM vs Native – Quick Comparison
| Aspect | JVM App | Native Image |
|---|---|---|
| Startup Time | 2–3s | ~50ms |
| Memory Usage | 150–300MB | 30–60MB |
| Binary Size | – | 40–80MB |
| Throughput (long-running) | Higher (JIT optimizations) | Slightly lower |
| Best Use Case | Batch jobs, data processing | Microservices, serverless, CLIs |
Tip: Don’t Go “Native” Everywhere
It’s tempting to rebuild everything as a native image, but native images are not a silver bullet.
- ✅ Perfect for cloud microservices, serverless functions, and edge deployments.
- ❌ Not always ideal for long-running applications where JVM’s JIT gives better throughput.
Think of native images as turbo-charged engines: amazing for quick acceleration, but not always best for long highway drives.
Final Thoughts
GraalVM native images bring Java into the serverless and cloud-native era. By combining Spring Boot 3, AOT processing, and careful dependency management, you can create apps that are faster, lighter, and cheaper to run in the cloud.
The best practice? Mix and match. Use JVM apps where throughput is king, and native images where startup time and memory efficiency matter most.
Your future Spring Boot deployments might not just run faster—they might also run smarter.






For long-running applications, one can use profile-guided optimizations (PGO) to achieve also the best throughput with GraalVM native images. With this setting, also long-running applications benefit from better execution characteristics with native image compared to the JVM. Find more information about this here: https://www.graalvm.org/latest/reference-manual/native-image/guides/optimize-native-executable-with-pgo/