# 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](https://ankurm.com/spring-boot-jdk-aot-cache-jep-514-515-startup-benchmarks/)** on [ankurm.com](https://ankurm.com). Everything the article quotes is a file in [`output/`](output), regenerated by [`run.sh`](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/`](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`](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`](output/00-prepare.txt), [`00-environment.txt`](output/00-environment.txt) | | [`scripts/native.sh`](scripts/native.sh) | Compiles the GraalVM native image | [`03-native-build.txt`](output/03-native-build.txt) | | [`scripts/train.sh`](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`](output/01-training.txt) | | [`scripts/startup.sh`](scripts/startup.sh) | 12 configurations x 10 interleaved rounds: time to ready, Boot's own figure, first request, resident memory | [`02-startup.txt`](output/02-startup.txt) | | [`scripts/mismatch.sh`](scripts/mismatch.sh) | 24 ways the cache and the run can disagree, and what the JVM says and does | [`04-mismatch.txt`](output/04-mismatch.txt) | | [`scripts/layers.sh`](scripts/layers.sh) | Docker layer arithmetic without Docker: which layers change on a one-line edit, sizes, gzipped sizes | [`05-layers.txt`](output/05-layers.txt) | | [`scripts/sample.sh`](scripts/sample.sh) | A transcript of the service's endpoints | [`06-sample-endpoints.txt`](output/06-sample-endpoints.txt) | | [`docker/Dockerfile`](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`](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. ```console 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.