java

Spring Boot 3 GraalVM Native Image Guide to Fix Cold Starts Fast

Learn how Spring Boot 3 with GraalVM Native Image cuts cold starts, memory use, and serverless costs. Build faster Java apps today.

Spring Boot 3 GraalVM Native Image Guide to Fix Cold Starts Fast

I want you to imagine waking up at 3 AM because a production alert just fired. Your serverless function is timing out. Users are complaining about slow responses. You check the logs—the cold start took six seconds. Six seconds of pure Java class loading, JIT compilation, and Spring context initialization. Each new request triggered that same penalty. The traditional JVM, with all its runtime magic, was costing you real money and real users.

I faced this problem last year. I had a microservice that needed to scale to zero and start in under 100 milliseconds. The usual solution was to keep a warm pool of JVMs running. That meant paying for idle compute. I needed something better. That something was GraalVM Native Image.

GraalVM compiles your Java application ahead of time into a standalone native executable. No JVM, no classpath scanning, no slow warmup. The result starts in milliseconds and uses a fraction of the memory. But there is a catch—GraalVM works under a closed-world assumption. It must know everything your application will ever do at build time. This clashes with Java’s dynamic nature. Reflection, dynamic proxies, resources loaded from classpath—all of these need explicit hints.

What does that mean for a Spring Boot 3 developer? It means you have to help the compiler see all the dynamic pieces. Spring Boot 3 introduced the AOT engine to do exactly that. It runs during the build, analyzes your beans, and generates the required configuration. But you still need to handle edge cases.

Let me walk you through building a native image with Spring Boot 3 from scratch. I will share my own struggles and the simple fixes that worked.

First, get the right JDK. I use GraalVM CE 21 via SDKMAN. Verify it works:

sdk install java 21.0.2-graalce
sdk use java 21.0.2-graalce
native-image --version

Now create a Spring Boot 3 project with the native profile. The easiest way is to use start.spring.io with dependencies: Web, Data JPA, PostgreSQL, Validation, Actuator, and (very important) GraalVM Native Support.

Look at the generated pom.xml. Notice the spring-boot-starter-parent and the native-maven-plugin. The plugin invokes GraalVM’s native-image tool during the native Maven profile. To build the native executable, run:

mvn -Pnative native:compile

This will download the necessary agent, analyse your code, and produce an executable in target/. The first build takes minutes because GraalVM does intensive static analysis. Subsequent builds are faster thanks to caching.

You get a binary named after your artifact. On my machine, it’s target/native-demo — around 70 MB. Run it directly:

./target/native-demo

The startup time? Under 0.2 seconds. The heap usage? Less than 50 MB. How is that possible? The JVM is gone. All your classes are compiled into machine code and linked together. The application heap is pre-initialized from constants.

But real applications are not that simple. Let me give you a concrete example. My demo uses JPA with a PostgreSQL driver. The database driver uses reflection to load its classes. That fails at native image build time unless you provide hints.

Have you ever debugged a ClassNotFoundException in a native image? It’s frustrating because the class exists in your JAR but the GraalVM compiler didn’t see it as reachable. The solution is to add a runtime initialization hint. In your src/main/resources/META-INF/native-image/reflect-config.json:

[
  {
    "name": "org.postgresql.Driver",
    "allDeclaredMethods": true,
    "allPublicMethods": true
  }
]

But Spring Boot 3 helps here. With the AOT engine, many common frameworks are automatically configured. You can also use the @RegisterReflectionForBinding annotation on your main class.

I once spent an hour because my application used @Value annotations with SpEL expressions. SpEL relies on runtime reflection. The fix was to move those values to properties files and use @ConfigurationProperties instead. Always test your native build early.

Another trap: proxy classes. Spring uses JDK dynamic proxies for @Transactional and @Cacheable methods. GraalVM needs to know about those proxies ahead of time. The AOT engine generates proxy classes during build. But if you use @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS), you must provide explicit proxy hints.

Here is a personal rule I follow: after writing any new code, build the native image before committing. If it breaks, I fix it immediately. Waiting until the end of the sprint turns a small problem into a cascade of failures.

Let me show you how to containerize the native binary. Docker allows you to use a scratch image, which is just an empty filesystem plus your binary. No JRE, no Alpine, nothing else. This reduces image size to about 70 MB.

My Dockerfile:

FROM scratch
COPY target/native-demo /app
EXPOSE 8080
ENTRYPOINT ["/app"]

Build with:

docker build -t native-demo .
docker run -p 8080:8080 native-demo

The container starts in microseconds. Compare that to a traditional Spring Boot container running on a JRE—that image is 200+ MB and takes seconds to start.

Is native image always the right choice? What if your application uses a lot of runtime code generation, like JBoss Drools or Apache Freemarker? Those frameworks will require extensive hint configuration. Sometimes the effort is not worth the gain. Native images trade build-time complexity for runtime performance. For long-running applications, the JVM with JIT compilation may outperform a static native binary over time.

I built a native image for a high-throughput Kafka consumer. The startup time improved, but the throughput was only 90% of the JVM version. Why? The JVM profiles hot paths and optimizes them. Native images cannot do that. So you must benchmark.

When you run the native executable, can you still use all Spring Boot features? Yes, but with limitations. Spring Cloud Function and many serverless frameworks work well. However, if you rely on --add-opens JVM flags or custom agents, they won’t work. GraalVM does not support the Java agent interface.

Testing a native application requires a different mindset. The standard @SpringBootTest loads a full JVM context. For native testing, use the @SpringBootTest with native profile and the spring-boot-starter-test dependency. Run tests with Maven as usual—the test classes compile normally. But to test the actual native binary, you need integration tests that start the binary as a process and hit its endpoints.

I wrote a simple integration test using Testcontainers that launches the Docker image and calls the REST API:

@Testcontainers
class NativeDemoIntegrationTest {
    @Container
    static GenericContainer<?> app = new GenericContainer<>("native-demo:latest")
        .withExposedPorts(8080);

    @Test
    void shouldRespond() {
        String url = "http://" + app.getHost() + ":" + app.getMappedPort(8080) + "/hello";
        String response = RestTemplate.getForObject(url, String.class);
        assertThat(response).contains("Hello");
    }
}

This catches issues like missing environment variables or wrong port bindings.

I often ask myself: is this the future of Java development? For cloud-native workloads, yes. The cold start advantage alone justifies the extra build time. But for desktop or long-running server applications, the JVM still wins.

One more tip: use -H:+ReportExceptionStackTraces when building to get better error messages during build failures. Also, consider using -march=native to optimize for your CPU.

If you are reading this and thinking “I should try this on my project,” start with a small service. Maybe a simple REST API with no database. Get the native build working. Then add one dependency at a time. Debug each failure.

I remember the first time I saw a native Spring Boot app start in 50 ms. I felt like Java had finally become a first-class citizen in the serverless world. No more cold starts, no more paying for idle JVM memory. Just pure, static, fast code.

Now I want you to take action. Open your Spring Boot 3 project. Add the GraalVM native support. Build it with the native profile. See the numbers for yourself. If you hit a roadblock, search for the error and add the appropriate hint.

Share your experience in the comments below. Did your startup time improve? How much memory did you save? I read every comment and try to help.

If you found this helpful, like the article. It tells me you want more content like this. And share it with a teammate who complains about slow startups. They will thank you.

Let’s make Java fast again, one native image at a time.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: Spring Boot 3, GraalVM Native Image, Java cold starts, serverless Java, native image build



Similar Posts
Blog Image
Master Event-Driven Microservices: Spring Cloud Stream and Kafka Complete Implementation Guide 2024

Master event-driven architecture with Spring Cloud Stream and Kafka. Learn microservices communication, fault tolerance, testing, and optimization techniques for scalable systems.

Blog Image
Complete Guide: Distributed Caching with Redis and Spring Boot Using Cache-Aside Pattern

Learn to implement distributed caching with Redis and Spring Boot using Cache-Aside pattern and reactive programming. Complete guide with code examples.

Blog Image
Redis Spring Boot Distributed Caching Guide: Cache-Aside and Write-Through Patterns with Performance Optimization

Master Redis distributed caching in Spring Boot with cache-aside and write-through patterns. Complete guide with connection pooling, performance optimization tips.

Blog Image
Apache Kafka Spring Security Integration: Building Secure Event-Driven Microservices with Authentication and Authorization Controls

Learn to integrate Apache Kafka with Spring Security for bulletproof event-driven architectures. Master authentication, authorization, and ACLs for secure microservices.

Blog Image
Apache Kafka Spring Cloud Stream Integration Guide: Build Scalable Event-Driven Microservices Architecture

Learn how to integrate Apache Kafka with Spring Cloud Stream for scalable event-driven microservices. Build robust, real-time messaging systems effortlessly.

Blog Image
Building Event-Driven Microservices with Spring Cloud Stream and Apache Kafka Complete Implementation Guide

Learn to build scalable event-driven microservices with Spring Cloud Stream and Apache Kafka. Complete guide with code examples, best practices, and production tips.