Files
spring-boot-demo/aot-cache
Claude 3e2029ab3a Add aot-cache module: the JDK 25 AOT cache on Spring Boot 4.1 vs AppCDS, Spring AOT and GraalVM native
A Spring Boot 4.1.1 order service started 12 ways (plain and extracted jar, Spring AOT output, AppCDS, AOT cache trained with and without traffic, JDK 27, native image), ten interleaved rounds each, with first-request latency; 24 cache-mismatch cases; Docker layer arithmetic. Transcripts are in output/ (no docs/ folder).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
2026-09-24 16:44:16 +00:00
..

aot-cache: the JDK 25 AOT cache on a Spring Boot 4.1 service

Companion project for Faster Spring Boot Startup with the JDK AOT Cache (JEP 514/515): Benchmarks vs CDS and Native on ankurm.com. Everything the article quotes is a file in output/, regenerated by run.sh. The explanation lives in the article; this directory holds the code and the raw transcripts. There is no docs/ folder on purpose: the deeper material sits in collapsible sections of the article itself.

The service

A small Spring Boot 4.1.1 web application, built to have the ingredients of a typical service and no more: Spring MVC and Jackson (POST /orders with a JSON body), Bean Validation (a record with @Email, @Pattern, @Min/@Max), the actuator health endpoint, and an in-memory store. No database. Sources are under src/main/java/com/ankurm/aotcache/.

Versions

Spring Boot 4.1.1 (Spring Framework 7.0.9, Tomcat 11.0.24, Jackson 3.1.5, Hibernate Validator 9.1.3)
JDK for the article Temurin 25.0.4.1 (LTS)
JDK for the comparison Temurin 27+35
GraalVM (native image) Community Edition 25.0.2+10.1
Machine 2 vCPU, 8 GB, Linux

What is here

Script What it does Output
scripts/prepare.sh Builds the plain jar, a jar with Spring's own AOT output, a jar after a one-line change; extracts them 00-prepare.txt, 00-environment.txt
scripts/native.sh Compiles the GraalVM native image 03-native-build.txt
scripts/train.sh The training runs: AppCDS, AOT cache (context-only and with traffic, with and without Spring AOT, on JDK 27, under ZGC) 01-training.txt
scripts/startup.sh 12 configurations x 10 interleaved rounds: time to ready, Boot's own figure, first request, resident memory 02-startup.txt
scripts/mismatch.sh 24 ways the cache and the run can disagree, and what the JVM says and does 04-mismatch.txt
scripts/layers.sh Docker layer arithmetic without Docker: which layers change on a one-line edit, sizes, gzipped sizes 05-layers.txt
scripts/sample.sh A transcript of the service's endpoints 06-sample-endpoints.txt
docker/Dockerfile A layered image with the cache trained in the final stage (not built here: no Docker daemon)

Headline result

Median of ten runs, milliseconds from starting the process to the first 200 from GET /ping (output/02-startup.txt):

Configuration Ready First POST /orders
Plain JVM, extracted jar 3,920 173
AppCDS 2,583 164
AOT cache, context-only training 2,022 143
AOT cache, training with traffic 1,949 68
AOT cache + Spring AOT, training with traffic 1,436 89
GraalVM native image 133 5.3

Running it

Needs JDK 25, JDK 27, a GraalVM for JDK 25 and Maven; scripts/env.sh shows where they are looked up and each can be overridden with JDK25, JDK27 and GRAALVM. The measurement scripts assume Linux and curl, and need the machine to themselves.

cd aot-cache
./run.sh                 # about 35 minutes; the native build is 5 of them
ROUNDS=3 ./run.sh        # a quick pass

Caveats

  • One small service on one 2-CPU virtual machine. The ratios are what to take away, not the milliseconds.
  • No database and no JPA, so the service is smaller than most real ones; a larger classpath usually gains more from the cache.
  • JEP 515 (method profiles) is part of the cache on JDK 25 and cannot be switched off separately, so its own contribution was not isolated.
  • The Docker image was not built; the layer figures come from extracting the jar's layers and comparing files.
  • openjdk.org/jeps returned HTTP 403 to the tooling used, so JEP claims come from running the JDKs and from secondary sources named in the article.