framework-benchmarks: Spring Boot 4.1.1 vs Quarkus 3.40.1 vs Micronaut 5.2.2, JVM + native, benchmarked

This commit is contained in:
2026-10-08 04:28:50 +00:00
commit 98e01bed4b
34 changed files with 1318 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
# 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 `<details>` 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