Spring Boot
Spring Boot rules and best practices for Claude Code.
13.4k
- Spring Boot
- Spring
- Java
- GraalVM
- Postgres
- Docker
- Kotlin
- Hibernate
6 TEMPLATES
Spring Boot 3 Modern Rules
# CLAUDE.md — Spring Boot 3 Modern Rules ## Version baseline - Spring Boot 3.2+ (3.4 if you can adopt). Java 21. - Migrate `javax.*` → `jakarta.*` once and for all. Boot 3 dropped `javax`. - Pin the Spring Boot parent in `pom.xml` / `plugins { id 'org.springframework.boot' version '...' }`. Don't manually align Spring deps. ## Project layout ``` src/main/java/com/app/ Application.java # @SpringBootApplication config/ domain/ api/ # controllers service/ repository/ Application.java src/main/resources/ application.yml # default config application-dev.yml # profile-specific overrides application-prod.yml ``` - Avoid splitting "common" beans across many modules. Boot's autoconfig discovers what's on the classpath. ## Configuration properties - `@ConfigurationProperties` over `@Value`. Use records: ```java @ConfigurationProperties("app.email") record EmailProperties(String fromAddress, String smtpHost, int smtpPort) {} ``` - `@EnableConfigurationProperties(EmailProperties.class)` once on a `@Configuration` class. - Validate with `@Validated` + `@NotNull` / `@NotEmpty` on fields. Boot fails fast on bad config. ## Profiles - `dev`, `test`, `prod`. Activate with `SPRING_PROFILES_ACTIVE=prod` (env var) or `--spring.profiles.active=prod`. - Don't have profile-specific Java code (`if (env.equals("dev"))`). Use `@Profile` on beans. - `application-{profile}.yml` overrides `application.yml`. Keep secrets out — use env vars or a secret manager. ## Logging - Default to **Logback** (Boot's default). Use **structured JSON** (logstash-encoder) in production. - `LOG_LEVEL_app=DEBUG` env var overrides at runtime (Boot reads logging.level.* env vars). - One logger per class: `private static final Logger log = LoggerFactory.getLogger(MyService.class);`. - Don't log sensitive data (PII, tokens). Add a redaction filter if there's any risk. ## Auto-configuration - Boot configures DataSource, JPA, Web, Security, etc. based on what's on the classpath. - Override defaults with explicit beans — they win over auto-config. - Disable specific auto-config: `@SpringBootApplication(exclude = SecurityAutoConfiguration.class)` only when you have a real reason. ## Starters - `spring-boot-starter-web` for Servlet stack, `spring-boot-starter-webflux` for reactive. Pick one. - `spring-boot-starter-data-jpa` brings Hibernate + Spring Data JPA + a connection pool. - `spring-boot-starter-actuator` for ops endpoints. Always include in production. ## Health & lifecycle - Boot's `/actuator/health` endpoint is on by default. Add component-specific checks via `HealthIndicator`. - Configure `management.endpoint.health.probes.enabled=true` for K8s liveness/readiness split. - Graceful shutdown: `server.shutdown=graceful`, `spring.lifecycle.timeout-per-shutdown-phase=30s`. Drain in-flight requests on SIGTERM. ## Don't - Don't `application.yml` your way to a 500-line config. Split by profile or extract to `@ConfigurationProperties`. - Don't disable Spring's exception handling to "see the real error". Use a `@ControllerAdvice` instead. - Don't run with `spring.jpa.hibernate.ddl-auto=update` in production. Use Flyway or Liquibase. - Don't put `@Bean` methods in your `Application.java`. Move to a dedicated `@Configuration` class.Spring Boot REST API
# CLAUDE.md — Spring Boot REST API ## Endpoint design - One controller per resource. Use kebab-case URLs and stable plurals (`/users`, `/orders`, `/order-items`). - HTTP verbs map to operations: `GET` (read), `POST` (create), `PUT` (replace), `PATCH` (partial), `DELETE` (delete). - Use `@RestController` + the verb-specific shortcuts (`@GetMapping`, `@PostMapping`). ## DTO layer - Don't expose JPA entities. Map to records: ```java record UserResponse(String id, String email, Instant createdAt) { static UserResponse from(User u) { return new UserResponse(u.getId(), u.getEmail(), u.getCreatedAt()); } } ``` - Separate request and response DTOs. They evolve independently. - Use **MapStruct** for tedious mapping when there's enough of it. Otherwise hand-write — clearer than reflection-based mappers. ## Validation - Bean Validation on request DTOs. `@Valid` on the controller parameter: ```java @PostMapping ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest req) { ... } ``` - Custom constraint annotations for cross-field rules. Don't validate in services if it could be at the DTO level. ## Error responses - Use **Problem Details for HTTP APIs (RFC 7807)** — `ProblemDetail` is built into Spring 6. - One `@RestControllerAdvice` per app for centralized error mapping: ```java @ExceptionHandler(NotFoundException.class) ProblemDetail handle(NotFoundException e) { var pd = ProblemDetail.forStatus(404); pd.setTitle("Not Found"); pd.setDetail(e.getMessage()); return pd; } ``` - Don't leak stack traces to clients. Log them, return a clean problem detail. ## OpenAPI / Swagger - **springdoc-openapi-starter-webmvc-ui** generates OpenAPI 3 from your controllers. - Annotate when the inferred docs are wrong: `@Operation`, `@ApiResponse`, `@Schema`. - Pin the spec version in CI — break PRs that change the API surface unintentionally. ## Pagination & filtering - Accept `Pageable` directly. Spring binds `?page=0&size=20&sort=createdAt,desc`. - For filtered list endpoints, use a Specification or QueryDSL — don't write `if (params.get("status") != null)` chains. - Cap page size in code: `if (pageable.getPageSize() > 100) pageable = ...withMaxSize(100);`. ## Versioning - URL versioning: `/api/v1/users`. Simple, cacheable, easy for clients. - Don't version via media type unless you have a strong reason (it complicates clients). - Keep `v1` working when you add `v2`. Deprecate explicitly via headers and changelogs. ## Idempotency - For non-idempotent verbs (`POST`, `PATCH`), accept an `Idempotency-Key` header. Store key→result for retries. - `PUT` should be naturally idempotent. If it isn't, you're using the wrong verb. ## Don't - Don't return `null` from a controller. Throw a domain exception or return `ResponseEntity.notFound().build()`. - Don't accept `Map<String, Object>` as a request body. Use a typed DTO. - Don't put business logic in the controller. Parse → call service → return. - Don't expose IDs you don't want enumerated. Use UUIDs for public-facing identifiers.Spring Boot Testing
# CLAUDE.md — Spring Boot Testing ## Test layers - **Unit tests** — plain JUnit + Mockito. No Spring context. Fast. - **Slice tests** — `@WebMvcTest`, `@DataJpaTest`, `@WebFluxTest`. Load only what's needed for one layer. - **Integration tests** — `@SpringBootTest` with `@AutoConfigureMockMvc` or full HTTP via `TestRestTemplate`/`WebTestClient`. Most tests should be unit or slice tests. `@SpringBootTest` is slow — use it sparingly, for end-to-end smoke tests. ## @WebMvcTest - Loads only MVC infrastructure + the controller under test. - Mock the service layer with `@MockBean`. - `MockMvc` for assertions: ```java mvc.perform(get("/users/{id}", "123")) .andExpect(status().isOk()) .andExpect(jsonPath("$.email").value("a@b")); ``` - Don't `@SpringBootTest` to test a single controller. Use `@WebMvcTest`. ## @DataJpaTest - Loads JPA + repositories + an in-memory or testcontainers DB. - Each test runs in a transaction that rolls back. Don't `commit()`. - Use **Testcontainers Postgres**, not H2. The dialect differences matter: ```java @Testcontainers @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class UserRepositoryTest { ... } ``` ## @SpringBootTest - Full app context. Use for end-to-end flows that span multiple layers. - `webEnvironment = RANDOM_PORT` and `TestRestTemplate`/`WebTestClient` for real HTTP. - Override beans with `@TestConfiguration` or `@MockBean` — don't fork production wiring with profiles like `test`. ## Testcontainers - One container per type per test run. Reuse with `@Container` static + `withReuse(true)`: ```java static PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine") .withReuse(true); static { POSTGRES.start(); } ``` - Configure via `@DynamicPropertySource`: ```java @DynamicPropertySource static void props(DynamicPropertyRegistry r) { r.add("spring.datasource.url", POSTGRES::getJdbcUrl); } ``` ## Test slice tips - `@MockBean` adds a mock to the application context. Differs from `@Mock`, which is just a Mockito mock. - Avoid `@MockBean` in unit tests — it forces context loading. Plain `@Mock` is faster. - `@SpyBean` for partial mocking. Use sparingly — usually a code smell. ## Speed - Reuse the application context across tests. Boot caches contexts by configuration; don't make every test unique. - `@DirtiesContext` invalidates the cache. Use it only when you must — it kills performance. - Run unit tests in parallel: `junit.jupiter.execution.parallel.enabled=true`. ## Don't - Don't load the full `@SpringBootTest` context for tests that touch one bean. - Don't use H2 / HSQLDB for repository tests. Use Postgres via Testcontainers. - Don't share state across tests. Random execution order is the default. - Don't `Thread.sleep` to wait for async work. Use `Awaitility` or `CompletableFuture.get(timeout)`.Spring Boot Actuator & Observability
# CLAUDE.md — Spring Boot Actuator & Observability ## Actuator - Add `spring-boot-starter-actuator`. It's mandatory in production. - Default exposed endpoints: `/actuator/health`, `/actuator/info`. Everything else is opt-in. - Expose only what's needed: `management.endpoints.web.exposure.include=health,info,metrics,prometheus`. - **Never** expose `env`, `heapdump`, `threaddump`, or `loggers` to public traffic. Lock them behind admin auth or a separate management port. ## Health checks - Boot composes `/actuator/health` from `HealthIndicator` beans (DB, Redis, etc.). - Implement custom `HealthIndicator` for app-specific dependencies: ```java @Component class StripeHealthIndicator implements HealthIndicator { public Health health() { ... } } ``` - For Kubernetes, enable probes split: `management.endpoint.health.probes.enabled=true`. - `/actuator/health/liveness` — process is alive (don't depend on DB here) - `/actuator/health/readiness` — can serve traffic (DB + cache reachable) ## Metrics with Micrometer - Micrometer is auto-configured. Add a Prometheus registry: `io.micrometer:micrometer-registry-prometheus`. - Tag every metric — high cardinality kills Prometheus. Tag by **service**, **endpoint**, **status** — never by user_id. - Standard metrics from Boot: HTTP request timings, JDBC pool, JVM, Tomcat. Custom metrics: ```java Timer timer = Timer.builder("checkout.duration").tag("flow", "card").register(meterRegistry); ``` ## Tracing - **Micrometer Tracing** with OpenTelemetry exporter. Add `micrometer-tracing-bridge-otel` + `opentelemetry-exporter-otlp`. - Sampling: 10% in production, 100% in dev. Configure with `management.tracing.sampling.probability`. - Propagate `traceparent` across HTTP, message queues, and async hops. Spring instruments most clients automatically. ## Logging - Structured JSON logs. Use **logstash-logback-encoder** or Boot's built-in JSON encoder (Boot 3.4+). - Add `trace_id` and `span_id` to every log line via MDC. Spring populates them when tracing is enabled. - One log statement per significant decision. Don't log every method entry. ## Profiling - **Spring Boot Profiler** (Boot 3.5+) for prod-safe sampling. - For deeper diagnostics, **JFR** (Java Flight Recorder) — built into the JVM, very low overhead. - `/actuator/heapdump` on demand for OOM investigation. Lock behind admin auth. ## Alerting - Define SLOs: `availability`, `p99 latency`, `error rate`. Alert when burned. - Don't alert on every spike. Multi-window, multi-burn-rate alerts (Google SRE book) are the right pattern. - Page on user-impacting symptoms, not on every CPU spike. ## Production checklist - Actuator on a separate port (`management.server.port=8081`) so internal-only endpoints don't share routing with public traffic. - Prometheus scraping `/actuator/prometheus`. - Logs shipped to Loki/Datadog/your aggregator. - Traces shipped to Tempo/Honeycomb/your tracer. ## Don't - Don't expose unauthenticated `/actuator/env` — leaks every config value, including resolved secrets. - Don't set `management.endpoints.web.exposure.include=*` in production. - Don't tag metrics by `user_id` or any high-cardinality value. Your bill will explode. - Don't bypass tracing for "performance" — it costs <1% and saves debugging hours.GraalVM + Spring Boot Native
# CLAUDE.md — GraalVM + Spring Boot Native ## Setup - Spring Boot 3.0+ has first-class native support via Spring AOT. - Add the GraalVM Native Build Tools: - Maven: enable the `native` profile in the parent POM (`spring-boot-starter-parent` ships it). - Gradle: apply `id 'org.graalvm.buildtools.native'`. - Build: - Maven: `./mvnw -Pnative native:compile` - Gradle: `./gradlew nativeCompile` - Container image build (no GraalVM locally needed): - Maven: `./mvnw -Pnative spring-boot:build-image` - Gradle: `./gradlew bootBuildImage` ## Spring AOT - AOT processing runs at build time. Produces source/config that bypasses runtime reflection. - Generated artifacts live in `target/spring-aot/` — don't edit, they regenerate. - AOT also runs in JVM mode (Boot 3+) for faster startup. You get the benefit without going native. ## Runtime hints - Spring auto-discovers most reflection. For your own custom code, register via `RuntimeHintsRegistrar`: ```java public class MyHints implements RuntimeHintsRegistrar { public void registerHints(RuntimeHints hints, ClassLoader cl) { hints.reflection().registerType(MyDto.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); hints.resources().registerPattern("data/*.json"); } } ``` - Register via `@ImportRuntimeHints(MyHints.class)` on a `@Configuration` class. ## What works in native - Spring MVC, WebFlux, Data JPA, Security, Actuator, JDBC. Most starters just work. - Tested with H2, Postgres, MySQL, MongoDB, Redis, Kafka, RabbitMQ. - For full status, see [Spring's native compatibility matrix](https://docs.spring.io/spring-boot/reference/packaging/native-image/index.html). ## What doesn't work - Mock-based testing in native (`@MockBean`) — use it in JVM, integration-test in native. - DevTools, Spring Boot Loader's reload — JVM-only. - Runtime bytecode manipulation libraries that don't ship native hints. ## Tracing agent - For libraries without first-class native support, use the agent during a JVM test run: ```sh java -agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/ -jar target/app.jar ``` - Run real test scenarios — the agent records reflection / resources used. ## Build optimization - Build is the bottleneck. Mitigate: - Cache `target/` and `~/.m2` in CI between runs. - Use `-march=native` only if the build host matches the deployment host architecture. Otherwise pick a target. - Build native only on release branches; PRs build JVM-mode (with AOT) for fast feedback. ## Image - Use distroless or minimal Debian: `FROM gcr.io/distroless/java-base-debian12`. - Don't bundle the full Java image — native binaries don't need a JVM. - Buildpacks produce a layered image automatically. ## Performance characteristics - Cold start: 50–200 ms vs 2–5 s on JVM. - Memory: ~80 MB vs ~250 MB for a typical Boot app. - Peak throughput: native is **slower** than JIT-warmed JVM for long-running services. Don't switch for throughput. ## Don't - Don't go native to "save memory" without measuring. JVM's `MaxRAMPercentage` + container limits often suffice. - Don't use `@MockBean` in native tests. They depend on bytecode manipulation. - Don't enable `spring.aot.enabled=false` in production. Lose AOT, lose the speedup. - Don't ignore tracing-agent output. The reflection config is doing real work.Spring Boot Deployment
# CLAUDE.md — Spring Boot Deployment ## Image strategy Three options, in order of preference: 1. **Cloud Native Buildpacks** — `mvn spring-boot:build-image` or `gradle bootBuildImage`. Layered, optimized, secure-by-default base. 2. **Layered jar + Dockerfile** — Spring's layered jar separates dependencies from app classes for better caching. 3. **GraalVM native image** — small startup, low memory; complex to debug. Use for serverless / Lambda. ## Buildpacks - One command, no Dockerfile: `./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:1.0`. - Pin the buildpack version: `<imageName>` config + Paketo builder version. - Configure JVM flags via `BPL_JVM_HEAD_ROOM`, `BPE_*` env vars at runtime. ## Layered jar Dockerfile ```dockerfile FROM eclipse-temurin:21-jre AS builder WORKDIR /app COPY target/*.jar app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --launcher FROM eclipse-temurin:21-jre WORKDIR /app COPY --from=builder /app/app/dependencies/ ./ COPY --from=builder /app/app/spring-boot-loader/ ./ COPY --from=builder /app/app/snapshot-dependencies/ ./ COPY --from=builder /app/app/application/ ./ USER nobody ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] ``` - Run as non-root. - The layer order is the cache order — dependencies change less than app code. ## Native image (GraalVM) - Add `org.graalvm.buildtools:native-maven-plugin` (or Gradle equivalent). - `./mvnw -Pnative native:compile` builds the native binary. - Reflection / dynamic proxies need hints. Spring Boot 3 generates most automatically; provide your own with `RuntimeHintsRegistrar` for edge cases. - Trade-off: 50–100ms startup, ~50 MB memory; build takes 1–5 minutes; harder to profile. ## JVM tuning - Container-aware JVM (Java 10+ does this automatically). `MaxRAMPercentage=75` is a good default. - `-XX:+ExitOnOutOfMemoryError` so the orchestrator restarts you instead of you limping along. - For Boot 3+, Spring AOT processing reduces startup time even on the JVM. Enable with `spring-boot-maven-plugin` `<process-aot>true</process-aot>`. ## Configuration - **12-factor**: every config from env vars. No files baked into the image. - Spring reads env vars natively: `SERVER_PORT=8080` overrides `server.port`. Use `SCREAMING_SNAKE_CASE`. - Secrets injected by the orchestrator (K8s Secrets, AWS Secrets Manager). Never bake. ## Migrations - Run **Flyway** or **Liquibase** as a separate one-shot job before the app starts. - For multi-replica deploys, never run migrations from `app boot` — race condition. - Schema changes are backward-compatible: add nullable column → deploy → backfill → make NOT NULL in next deploy. ## Health & graceful shutdown - Configure: `server.shutdown=graceful`, `spring.lifecycle.timeout-per-shutdown-phase=30s`. - Kubernetes: `terminationGracePeriodSeconds: 35` (slightly more than the shutdown timeout). - Liveness/readiness split, lock behind a management port. ## CI - Cache `.m2` / `.gradle` between runs. - Multi-stage CI: lint → test → build image → integration test against the image → push. - Sign images with `cosign` if your platform supports it. ## Don't - Don't run with `--add-opens` flags in production unless you know exactly which library needs them. Each one is a future migration. - Don't put `application-prod.yml` in version control with secrets in plaintext. - Don't forget `USER nobody` in your Dockerfile. Root containers are a footgun. - Don't try to fit a 1 GB JAR into a 256 MB pod. Profile memory before sizing.
Have a CLAUDE.md template that works for you?
Send it in — we’ll credit you and publish it under the right tags.