Quarkus + GraalVM Native
Native compilation, reflection config, resource inclusion, and image-size tuning.
# CLAUDE.md — Quarkus + GraalVM Native
## When to go native
- Cold-start matters: serverless (Lambda, Cloud Run cold starts), CLI tools, scale-to-zero deployments.
- Memory matters: tight container sizes, edge environments.
- **Don't** go native when:
- Throughput is the priority (JIT eventually beats native after warm-up)
- The team isn't ready for the build complexity
- You depend on libraries that need extensive reflection / dynamic class loading without Quarkus extensions
## Build
- `./mvnw package -Pnative` (or `./gradlew build -Dquarkus.package.type=native`).
- Use **Mandrel** (Red Hat's GraalVM distribution focused on native-image). Pin the version: `quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:23.1-java21`.
- Container build: `quarkus.native.container-build=true` — works without GraalVM installed locally.
- Build is slow (1–5 minutes). Optimize CI with cached Maven repo + builder image.
## Reflection & resources
- Quarkus extensions register most reflection automatically.
- For your own code that uses reflection (rare), register classes:
```java
@RegisterForReflection(targets = { com.app.SomeDto.class })
public class ReflectionConfig {}
```
- Resources accessed at runtime: `quarkus.native.resources.includes=path/to/file.json,**/*.properties`.
- Static initializers that touch the environment (file system, network) need `--initialize-at-run-time` hints — usually handled by Quarkus.
## Runtime gotchas
- Native binaries don't support agents. JFR works but with limitations.
- Reflection on classes not registered fails at runtime — catch in tests, not prod.
- Class-loading is closed-world. No `Class.forName(dynamicString)` patterns.
## Testing
- `@QuarkusIntegrationTest` runs against the packaged artifact (jar or native).
- Use `mvn verify -Pnative` to run integration tests against the native binary in CI.
- Tag native-only tests `@DisabledOnIntegrationTest` if they only make sense in JVM mode.
## Image size
- Mandrel base image is a few hundred MB. Final native binary ~50–100 MB.
- Strip the image with `quarkus.native.compression.level=10` for binary compression.
- Use `distroless/static` or `scratch` as the runtime image — copy in the binary, run it.
## Memory
- Native uses much less heap than JVM. Set `-Xmx256m` and watch — many apps run on 64–128 MB.
- The savings come from no JIT, no class metadata, no interpreter overhead.
## Don't
- Don't switch to native to "optimize" without measuring. JVM with Quarkus already starts in 1–2s and uses ~150 MB.
- Don't expect native + reflection-heavy library to "just work". Test the integration in native mode early.
- Don't skip running the native test suite in CI. The cost is real but cheaper than a prod surprise.
- Don't use `--no-fallback` until you're sure your app works fully native — the fallback image is a JVM bundled into the binary, not what you want.
Other Java templates
Modern Java Rules
Java 21+ defaults: records, sealed types, pattern matching, virtual threads, var.
Java Testing with JUnit 5
JUnit 5 + AssertJ + Mockito + Testcontainers — opinionated test conventions.
Java Virtual Threads & Concurrency
Project Loom virtual threads, structured concurrency, and modern concurrent patterns.
Java Build (Maven & Gradle)
Build conventions, dependency management, multi-module projects, and CI patterns.
Java Streams & Collections
Streams API, immutable collections, Collectors, and idiomatic data manipulation.