ADR-009: `service-java` stack defaults
Status: Accepted Date: 2026-05-22
Context
Section titled “Context”The service-java shape is one of two case studies validating the §4.1 add-a-shape contract from the platform vision PRD. It needs concrete defaults across four axes — build tool, container image, observability, and code style — so the Copier template and the svc-java plugin’s agents are unambiguous. These decisions cluster into one ADR rather than four because they together define the shape’s identity.
Decision
Section titled “Decision”The service-java template ships with:
| Axis | Choice |
|---|---|
| Language / runtime | JDK 21 LTS (pinned per ADR-005: Node version policy — pin current LTS, bump via copier update-style policy) |
| Framework | Spring Boot 3.x |
| Build tool | Maven |
| Container image base | gcr.io/distroless/java21-debian12:nonroot |
| Observability | Spring Boot Actuator + Micrometer (Prometheus registry) + OpenTelemetry Java agent (-javaagent) |
| Code style | Spotless + google-java-format, with a minimal Checkstyle config for what the formatter doesn’t enforce |
Build tool — Maven (chosen over Gradle)
Section titled “Build tool — Maven (chosen over Gradle)”- Maven’s POM is declarative XML — easier for the
coderagent to read, parse, and edit safely than a Kotlin/Groovy build script. - Spring Boot has first-class Maven support;
spring-boot-maven-plugincovers run, repackage, and image build. - Lower cognitive load for human contributors who haven’t worked with Gradle Kotlin DSL.
- Trade-off accepted: slower full builds than Gradle for large projects, more verbose POMs. For service-shaped repos (one module, modest dependency count) the gap is negligible.
Container image — Distroless JRE 21
Section titled “Container image — Distroless JRE 21”- ~250MB image; no shell; non-root by default.
- Matches the
service-godistroless/static-nonrootchoice — uniform security posture across backend shapes. - GraalVM native image rejected as default: longer build times, reflection requires explicit config (Spring AOT helps but still friction), harder debugging. May become an opt-in mode later for cold-start-sensitive services.
Observability — Actuator + Micrometer + OTel agent
Section titled “Observability — Actuator + Micrometer + OTel agent”- Actuator exposes
/actuator/health,/actuator/health/readiness,/actuator/info,/actuator/prometheus. - Micrometer’s Prometheus registry feeds the
/actuator/prometheusendpoint. - OpenTelemetry Java agent (attached via
-javaagentin the Dockerfile entrypoint) gives distributed tracing with zero code changes. - Most idiomatic Spring Boot setup; new contributors find documentation instantly.
Code style — Spotless + google-java-format
Section titled “Code style — Spotless + google-java-format”mvn spotless:applyruns indevbox run lint-fix;mvn spotless:checkruns indevbox run lintand CI.- google-java-format is the most widely adopted Java style — agents trained on public code produce conformant output.
- Minimal Checkstyle config catches things the formatter doesn’t (unused imports, naming, javadoc presence on public APIs).
Devbox recipes (canonical)
Section titled “Devbox recipes (canonical)”The template ships devbox.json with at least:
| Recipe | Wraps |
|---|---|
test |
mvn -B verify |
test-fast |
mvn -B test |
lint |
mvn -B spotless:check checkstyle:check |
lint-fix |
mvn -B spotless:apply |
quality |
lint then verify |
audit |
mvn -B org.owasp:dependency-check-maven:check |
image-build |
mvn -B spring-boot:build-image -Dspring-boot.build-image.imageName=<name>:scan |
image-scan |
trivy image --severity CRITICAL,HIGH <name>:scan |
dev |
mvn -B spring-boot:run |
deploy |
kubectl apply -k k8s/overlays/local/ |
Consequences
Section titled “Consequences”- The
svc-javaplugin’scoderagent generates Spring Boot 3.x code, edits POMs via clearly-bounded XML manipulation (ormavenplugin commands), and always invokesdevbox run— never rawmvn. - The
testeragent writes JUnit 5 + Spring Boot Test, uses Testcontainers when integration tests need real dependencies, and reads coverage from JaCoCo (configured in the POM). - The
deploymentagent’s Dockerfile is a multi-stage build: Maven build stage (maven:3.9-eclipse-temurin-21) → distroless runtime stage with the-javaagentline. - Re-evaluate Maven vs Gradle if the platform ever needs multi-module composite builds or build-time AOT (GraalVM native) becomes a default goal.
References
Section titled “References”- platform-vision.md §4, §11.8
- ADR-008: Shape detection —
.copier-answers.ymlwith sniffing fallback — sniffing fallback usespom.xmlto detect this shape
Amendment (implementation, phase 6)
Section titled “Amendment (implementation, phase 6)”Verified against a rendered project with real Maven/JDK runs; differences from the text above:
image-buildusesdocker build(the template’s multi-stage Dockerfile: Maven builder → distroless java21 with the-javaagent), notspring-boot:build-image— the “Consequences” section requires that Dockerfile, and it keeps every shape’simage-buildidentical. Buildpacks are not used.quality=lint+typecheck(mvn -DskipTests compile test-compile); the test run, including the JaCoCo 80% line gate (Application.classexcluded), isdevbox run test(mvn verify), matching the other shapes wherequalitydoes not run tests.- Actuator probes (
/actuator/health/liveness,/actuator/health/readiness) replace/health+/readyfor this shape; Kubernetes probes and the agents use them. - The OpenTelemetry agent is baked in but disabled (
OTEL_SDK_DISABLED=true) until an environment sets it tofalseand providesOTEL_EXPORTER_OTLP_ENDPOINT. audit(OWASP Dependency-Check) needs anNVD_API_KEYto be practical; CI reads it from a repository secret. This recipe was not executed during template verification (it downloads the full NVD database).
Amendment (found by the todo-app end-to-end test)
Section titled “Amendment (found by the todo-app end-to-end test)”devbox run auditno longer fails whenNVD_API_KEYis unset: an empty key made OWASP Dependency-Check abort with “Invalid API Key” in CI. With a key it runs Dependency-Check as before; without one it runstrivy fs(HIGH/CRITICAL,--exit-code 1). Accepted risks live in a project-owned.trivyignore.- A fresh Spring Boot 3.5.16 project failed that audit on day one: Tomcat 10.1.55 and Jackson 2.21.4 (BOM-managed) have fixed releases, so
pom.xmloverridestomcat.version(10.1.60) andjackson-bom.version(2.21.7); the Spring Framework 6.2 XsltView CVE-2026-47884 is fixed only in Spring 7 / Boot 4 and is listed in.trivyignorewith its rationale. Remove overrides as the parent catches up; revisit the ignore when moving to Boot 4.