# framework-benchmarks Three identical REST APIs - Spring Boot, Quarkus, Micronaut - built JVM and native, and benchmarked for cold-start time, resident memory, and throughput. Companion repo for the ankurm.com post "Spring Boot 4.1 vs Quarkus vs Micronaut: Startup, Memory and Throughput Benchmarked." No `docs/` chapter folder here by design - every finding below is written up in full in the post itself, inside per-section `
` accordions. This repo carries the runnable code and the raw captured output those sections link to. ## Versions under test | Component | Version | Verified against | |---|---|---| | Spring Boot | 4.1.1 | `spring-boot-dependencies` Maven Central metadata | | Quarkus | 3.40.1 (the 3.40 LTS stream) | `quarkus-bom` Maven Central metadata + quarkus.io/releases | | Micronaut | 5.2.2 | `micronaut-platform` Maven Central metadata | | GraalVM | CE 25.4.4.1.1 (OpenJDK 25.0.4.1.1) | graalvm/graalvm-ce-builds GitHub releases (latest at build time) | | JDK baseline | 25 (current LTS) | all three apps compiled and run on the same GraalVM JDK 25 | ## Quickstart ```bash # JVM mode cd spring-boot-app && mvn -q package -DskipTests && java -jar target/spring-boot-app.jar # :8081 cd quarkus-app && mvn -q package -DskipTests && java -jar target/quarkus-app/quarkus-run.jar # :8082 cd micronaut-app && mvn -q package -DskipTests && java -jar target/micronaut-app.jar # :8083 # Native mode (needs GraalVM's native-image on PATH; budget ~7-8 min per app) cd spring-boot-app && mvn -q -Pnative package -DskipTests && mvn -q -Pnative native:compile && ./target/spring-boot-app cd quarkus-app && mvn -q -Pnative clean package -DskipTests && ./target/quarkus-app-runner cd micronaut-app && mvn -q -Pnative clean package -DskipTests \ && mvn -q -Pnative package org.graalvm.buildtools:native-maven-plugin:compile -DskipTests \ && ./target/micronaut-app ``` `scripts/run-all.sh` does all of the above plus every benchmark, end to end, and regenerates every file in `docs/output/`. ## Modules | Module | Framework | JVM port | Build profile for native | |---|---|---|---| | `spring-boot-app` | Spring Boot 4.1.1, Tomcat, Spring MVC | 8081 | `-Pnative`, then explicit `native:compile` (see below) | | `quarkus-app` | Quarkus 3.40.1, Quarkus REST (RESTEasy Reactive), Vert.x | 8082 | `-Pnative` | | `micronaut-app` | Micronaut 5.2.2, Netty, Micronaut Serialization (compile-time, not reflection) | 8083 | `-Pnative` + explicit `native-maven-plugin:compile` (see below) | All three expose the identical endpoints, same JSON shapes, same seed data, no database: | Method | Path | Behaviour | |---|---|---| | `GET` | `/widgets` | list all widgets (seeded with 5) | | `GET` | `/widgets/{id}` | one widget, 404 if unknown | | `POST` | `/widgets` | create (`{"name": "...", "quantity": N}`), 400 on blank name or negative quantity | | `GET` | `/health` (Micronaut) / `/actuator/health` (Spring Boot) / `/q/health` (Quarkus) | trivial liveness check - deliberately each framework's own normal way to add one, not forced to be identical, since that difference is itself one of the post's findings | No database on purpose: it isolates framework/runtime startup overhead from JDBC driver and native-image reflection-metadata concerns, which is a separate, larger topic than this post covers. ## The two real build failures this repo hit (and the fixes, committed as-is) 1. **Micronaut silently ignored `application.yml`.** `micronaut-yaml` (which wraps snakeyaml) isn't a transitive dependency of `micronaut-http-server-netty` - without it, Micronaut skips YAML config files with no warning and falls back to every default (here, port 8080 instead of the configured 8083). Fixed by using `application.properties` instead, matching the other two modules. See the post's Micronaut section for the full story. 2. **Micronaut's native-image build failed four times in a row** on a Netty/logback static-initialization clash before `--initialize-at-run-time=io.netty` (the whole package) fixed it. All four real `native-image` error messages are in `docs/output/06-micronaut-native-build-failures.txt`, verbatim. The working build argument lives in `micronaut-app/pom.xml`'s `native` profile. Also note: neither `native-maven-plugin` nor Spring Boot's own native profile auto-binds `native-image` to a Maven phase in a plain multi-module layout the way `mvn -Pnative package` alone implies - both modules need the goal invoked explicitly (`native:compile`, or Micronaut's `native-maven-plugin:compile`). See `scripts/run-all.sh` for the exact commands that actually trigger a native build end to end. ## Captured output | File | What it is | |---|---| | `docs/output/01-startup-memory-jvm.txt` | All 3 frameworks, JVM mode, 4 cold-start runs each, real `measure.py` JSON | | `docs/output/02-throughput-jvm.txt` | All 3 frameworks, JVM mode, 15s/20-concurrency throughput runs | | `docs/output/03-startup-memory-native.txt` | Same as 01, native mode | | `docs/output/04-throughput-native.txt` | Same as 02, native mode | | `docs/output/05-summary.txt` | Consolidated table, hand-written from the four files above | | `docs/output/06-micronaut-native-build-failures.txt` | The four real native-image build attempts for Micronaut, verbatim | `scripts/measure.py` and `scripts/throughput.py` are dependency-free Python (stdlib only) - no `wrk`/`hey`/`ab` install required to reproduce any number in this repo. ## License MIT