assets/Dockerfile
# syntax=docker/dockerfile:1
#
# Multi-stage JVM image for a Spring Boot Maven project.
# Adapted from jdubois/dr-jskill (Apache-2.0); see references/PROVENANCE.md.
#
# - The Boot jar is exploded into layers and started with the JarLauncher.
# - A minimal runtime is assembled with jlink.
# - The runtime image is distroless (no shell, non-root, uid 65532).
# JVM flags go through JAVA_TOOL_OPTIONS because there is no shell.
#
# docker build -t my-spring-app .
# docker run --rm -p 8080:8080 my-spring-app
FROM eclipse-temurin:25-jdk-noble AS build
WORKDIR /app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
RUN chmod +x mvnw && ./mvnw dependency:go-offline -B
COPY src ./src
RUN ./mvnw clean package -DskipTests -B
RUN cp target/*.jar app.jar && \
java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted
# Module list suits typical web + JPA apps; run `jdeps --print-module-deps` if the app fails with NoClassDefFoundError.
RUN "$JAVA_HOME/bin/jlink" \
--add-modules java.base,java.logging,java.naming,java.management,java.management.rmi,java.instrument,java.sql,java.sql.rowset,java.transaction.xa,java.xml,java.xml.crypto,java.desktop,java.security.jgss,java.security.sasl,java.scripting,java.compiler,java.net.http,java.rmi,java.prefs,jdk.unsupported,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.jfr,jdk.management,jdk.management.agent,jdk.attach,jdk.zipfs,jdk.charsets,jdk.net,jdk.security.auth \
--strip-debug --no-man-pages --no-header-files --compress=zip-6 \
--output /javaruntime
FROM gcr.io/distroless/base-debian12:nonroot
ENV JAVA_HOME=/opt/java
ENV PATH="$JAVA_HOME/bin:$PATH"
COPY --from=build /javaruntime $JAVA_HOME
WORKDIR /app
COPY --from=build --chown=65532:65532 /app/extracted/dependencies/ ./
COPY --from=build --chown=65532:65532 /app/extracted/spring-boot-loader/ ./
COPY --from=build --chown=65532:65532 /app/extracted/snapshot-dependencies/ ./
COPY --from=build --chown=65532:65532 /app/extracted/application/ ./
USER nonroot
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
EXPOSE 8080
ENTRYPOINT ["/opt/java/bin/java", "org.springframework.boot.loader.launch.JarLauncher"]
assets/Dockerfile-gradle
# syntax=docker/dockerfile:1
#
# Multi-stage JVM image for a Spring Boot Gradle project (Groovy or Kotlin DSL).
# Gradle counterpart of ./Dockerfile; the toolchain in the build must match the JDK of the build stage.
# Adapted from jdubois/dr-jskill (Apache-2.0); see references/PROVENANCE.md.
#
# - The Boot jar is exploded into layers and started with the JarLauncher.
# - A minimal runtime is assembled with jlink.
# - The runtime image is distroless (no shell, non-root, uid 65532).
# JVM flags go through JAVA_TOOL_OPTIONS because there is no shell.
#
# docker build -f Dockerfile-gradle -t my-spring-app .
# docker run --rm -p 8080:8080 my-spring-app
FROM eclipse-temurin:25-jdk-noble AS build
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY settings.gradle* build.gradle* ./
RUN chmod +x gradlew && ./gradlew --no-daemon dependencies > /dev/null
COPY src ./src
RUN ./gradlew --no-daemon bootJar -x test
# bootJar also leaves a *-plain.jar next to the executable jar: skip it.
RUN cp "$(ls build/libs/*.jar | grep -v -- '-plain\.jar$' | head -1)" app.jar && \
java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted
# Module list suits typical web + JPA apps; run `jdeps --print-module-deps` if the app fails with NoClassDefFoundError.
RUN "$JAVA_HOME/bin/jlink" \
--add-modules java.base,java.logging,java.naming,java.management,java.management.rmi,java.instrument,java.sql,java.sql.rowset,java.transaction.xa,java.xml,java.xml.crypto,java.desktop,java.security.jgss,java.security.sasl,java.scripting,java.compiler,java.net.http,java.rmi,java.prefs,jdk.unsupported,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.jfr,jdk.management,jdk.management.agent,jdk.attach,jdk.zipfs,jdk.charsets,jdk.net,jdk.security.auth \
--strip-debug --no-man-pages --no-header-files --compress=zip-6 \
--output /javaruntime
FROM gcr.io/distroless/base-debian12:nonroot
ENV JAVA_HOME=/opt/java
ENV PATH="$JAVA_HOME/bin:$PATH"
COPY --from=build /javaruntime $JAVA_HOME
WORKDIR /app
COPY --from=build --chown=65532:65532 /app/extracted/dependencies/ ./
COPY --from=build --chown=65532:65532 /app/extracted/spring-boot-loader/ ./
COPY --from=build --chown=65532:65532 /app/extracted/snapshot-dependencies/ ./
COPY --from=build --chown=65532:65532 /app/extracted/application/ ./
USER nonroot
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"
EXPOSE 8080
ENTRYPOINT ["/opt/java/bin/java", "org.springframework.boot.loader.launch.JarLauncher"]
assets/Dockerfile-native
# syntax=docker/dockerfile:1
#
# Multi-stage GraalVM native image for a Spring Boot Maven project.
# Adapted from jdubois/dr-jskill (Apache-2.0); see references/PROVENANCE.md.
# Requires the `native` dependency (native-maven-plugin and the `native` profile).
#
# docker build -f Dockerfile-native -t my-spring-app-native .
# docker run --rm -p 8080:8080 my-spring-app-native
FROM ghcr.io/graalvm/graalvm-community:25 AS build
WORKDIR /app
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
RUN chmod +x mvnw && ./mvnw dependency:go-offline -B
COPY src ./src
ENV NATIVE_IMAGE_OPTIONS="-H:+StaticExecutableWithDynamicLibC"
RUN ./mvnw -Pnative -DskipTests -B package native:compile
# The executable is named after the artifactId; copy it to a fixed name.
# GraalVM 25 also drops executable libjava.so/libjvm.so stubs next to it: skip them.
RUN native_exe=$(find target -maxdepth 1 -type f -executable ! -name '*.jar' ! -name '*.original' ! -name '*.so' | head -1) && \
test -n "$native_exe" && cp "$native_exe" native-app
FROM gcr.io/distroless/base-debian12:nonroot
WORKDIR /app
COPY --from=build --chown=nonroot:nonroot /app/native-app native-app
USER nonroot
EXPOSE 8080
ENTRYPOINT ["./native-app"]
assets/Dockerfile-native-gradle
# syntax=docker/dockerfile:1
#
# Multi-stage GraalVM native image for a Spring Boot Gradle project (Groovy or Kotlin DSL).
# Adapted from jdubois/dr-jskill (Apache-2.0); see references/PROVENANCE.md.
# Requires the `native` dependency (org.graalvm.buildtools.native plugin).
#
# docker build -f Dockerfile-native-gradle -t my-spring-app-native .
# docker run --rm -p 8080:8080 my-spring-app-native
FROM ghcr.io/graalvm/graalvm-community:25 AS build
WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY settings.gradle* build.gradle* ./
RUN chmod +x gradlew && ./gradlew --no-daemon dependencies > /dev/null
COPY src ./src
ENV NATIVE_IMAGE_OPTIONS="-H:+StaticExecutableWithDynamicLibC"
RUN ./gradlew --no-daemon nativeCompile
# The executable is named after the project; copy it to a fixed name.
# GraalVM 25 also drops executable libjava.so/libjvm.so stubs next to it: skip them.
RUN native_exe=$(find build/native/nativeCompile -maxdepth 1 -type f -executable ! -name '*.jar' ! -name '*.original' ! -name '*.so' | head -1) && \
test -n "$native_exe" && cp "$native_exe" native-app
FROM gcr.io/distroless/base-debian12:nonroot
WORKDIR /app
COPY --from=build --chown=nonroot:nonroot /app/native-app native-app
USER nonroot
EXPOSE 8080
ENTRYPOINT ["./native-app"]
references/configuration.md
# Configuration
## Format
Use `application.properties` or `application.yml`, **matching what the project already uses**. New projects generated by start.spring.io get `application.properties`; YAML is fine for deeply nested settings. Do not mix both in one project.
## Profiles
```properties
# application.properties (shared defaults)
spring.application.name=demo
spring.jpa.open-in-view=false
server.shutdown=graceful
```
- `application-dev.properties`, `application-prod.properties`, activated with `SPRING_PROFILES_ACTIVE` (environment variable) or `--spring.profiles.active`.
- Keep prod-safe values in the base file; override only what differs in dev (e.g. `logging.level.com.example=DEBUG`).
- Tests: use `src/test/resources/application.properties` or `@TestPropertySource`, not a "test" profile checked into `main`.
## Externalized values and secrets
- Read values from environment variables with defaults for local development: `spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/mydb}`.
- Spring Boot also maps `SPRING_DATASOURCE_URL` to `spring.datasource.url` automatically (relaxed binding).
- For a local `.env` file: `spring.config.import=optional:file:.env[.properties]`. Add `.env` to `.gitignore`; commit `.env.sample` with placeholder values.
- Never commit passwords, tokens or API keys. In production use the platform's secret store (Kubernetes secrets, Vault, cloud key vault) exposed as environment variables or config trees (`spring.config.import=configtree:/run/secrets/`).
## Type-safe properties
```java
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(@NotBlank String from, @Positive int retries) {}
```
Enable with `@ConfigurationPropertiesScan` on the application class (or `@EnableConfigurationProperties`). Prefer this over scattered `@Value`. Spring Boot's annotation processor (`spring-boot-configuration-processor`, optional dependency) generates IDE metadata.
## Checklist
- [ ] One format (properties or YAML) used consistently
- [ ] No secrets in the repository
- [ ] Defaults work locally; production values come from the environment
- [ ] `@ConfigurationProperties` with validation for custom settings
references/database.md
# Database (PostgreSQL)
Defaults: PostgreSQL (image `postgres:18-alpine`, see [versions.md](versions.md)), Spring Data JPA, Hibernate, HikariCP. Dependencies: `data-jpa`, `postgresql`, `docker-compose`, `testcontainers`.
## Configuration
```properties
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/mydb}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:user}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:password}
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false
spring.jpa.show-sql=false
```
The driver class is detected from the URL; do not set it. The defaults above are for local development only: production credentials come from the environment (see [configuration.md](configuration.md)).
## Schema management (`ddl-auto`)
Follow the schema management rule of the `java-hibernate-jpa` skill (prototype/local, production with Flyway/Liquibase, or externally managed). The table and setup below apply it to Spring Boot.
| Value | Behaviour | Use for |
|-------|-----------|---------|
| `update` | Adds missing tables/columns, never drops | Local development / prototypes |
| `validate` | Fails at startup if schema and entities differ | Production |
| `create` / `create-drop` | Recreates schema at startup (and drops at shutdown) | Throw-away tests |
| `none` | Hibernate does nothing | Schema managed elsewhere |
Hibernate's `update` is convenient but cannot rename, drop or migrate data. For production schema evolution, **Flyway or Liquibase are optional but recommended**: add the starter: on Boot 4 `spring-boot-starter-flyway` plus `org.flywaydb:flyway-database-postgresql` (or `spring-boot-starter-liquibase`), because Boot 4 auto-configuration lives in the starter; on Boot 3 add `org.flywaydb:flyway-core` plus `flyway-database-postgresql` (or `liquibase-core`) directly. Put migrations in `src/main/resources/db/migration` (`V1__init.sql`), and set `ddl-auto=validate`. Add them when the user wants versioned migrations or the app is headed to production.
## Entities
Keep entities annotated precisely (`@Table`, `@Column(length=..., nullable=...)`, `@Index`) so generated or validated DDL is accurate. Use `GenerationType.IDENTITY` or a sequence, `Instant` for timestamps, and avoid exposing entities in the API (use DTOs/records).
## Local development: Docker Compose
With `spring-boot-docker-compose`, Spring Boot starts `compose.yaml` automatically and wires the datasource (no URL needed):
```yaml
services:
postgres:
image: postgres:18-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
ports:
- "5432"
healthcheck:
test: ["CMD", "pg_isready", "-U", "user", "-d", "mydb"]
interval: 10s
timeout: 5s
retries: 5
```
## Tests
Use Testcontainers with `@ServiceConnection`; see the `spring-boot-testing` skill (`references/testcontainers-jdbc.md`). Pin the image tag; never `latest`. start.spring.io generates `postgres:latest` in `compose.yaml` and `TestcontainersConfiguration`: replace both with `postgres:18-alpine` (verified).
## Performance and production
- HikariCP is the default pool; tune `spring.datasource.hikari.maximum-pool-size` and `connection-timeout`.
- Avoid N+1 queries: `JOIN FETCH` or `@EntityGraph`; use DTO projections for read models.
- Keep `open-in-view=false` and `@Transactional` on services.
- Index foreign keys and filtered/sorted columns; check plans with `EXPLAIN ANALYZE`.
references/docker.md
# Docker
Four multi-stage Dockerfiles are provided: JVM and GraalVM native images, each for Maven and Gradle (Groovy or Kotlin DSL). Copy the one you need to the project root and build it with `-f`. AOT and CRaC images are deferred.
| | Maven | Gradle |
|--|-------|--------|
| JVM image | [../assets/Dockerfile](../assets/Dockerfile) | [../assets/Dockerfile-gradle](../assets/Dockerfile-gradle) |
| Native image | [../assets/Dockerfile-native](../assets/Dockerfile-native) | [../assets/Dockerfile-native-gradle](../assets/Dockerfile-native-gradle) |
Verified on Boot 4.1.1 (web + actuator + validation), arm64 Docker Desktop: all four build and serve `/hello` and `/actuator/health`. JVM images are about 200 MB and build in under a minute (about 2 s startup); native images are about 175 MB, take 2m20-2m55 to build (around 6 GB of builder memory) and start in about 0.09 s. The Gradle builds set the toolchain to the same Java version as the build stage (25).
## Choosing
| | JVM image | Native image |
|--|-----------|--------------|
| Build time | Fast | Slow, memory hungry (several GB) |
| Startup / memory | Seconds / higher | Milliseconds / low |
| Compatibility | Everything | Needs GraalVM-compatible libraries; reflection hints may be needed |
| Pick when | Default choice | Scale-to-zero, CLI-like, tight memory limits |
Native requires the `native` dependency at generation time (`-d dependencies=...,native`) and the Spring Boot `native` Maven profile (included by start.spring.io when the dependency is selected).
## Properties of the provided Dockerfiles
- Multi-stage: build in a JDK/GraalVM image, run in a small runtime image.
- JVM: the Boot jar is extracted into layers (`java -Djarmode=tools -jar app.jar extract --layers --launcher`) for better caching, runtime assembled with `jlink`, final base is distroless `nonroot` (no shell, no package manager).
- Native: the executable is built with `-H:+StaticExecutableWithDynamicLibC` and copied into distroless `nonroot`. The `find` that picks the executable must skip `*.so`: GraalVM 25 leaves executable `libjava.so`/`libjvm.so` stubs next to it, and copying one of those gives a container that exits with 139 (segfault).
- Run as non-root, expose 8080, JVM flags via `JAVA_TOOL_OPTIONS` (no shell to expand `JAVA_OPTS`).
- Pinned base image tags (`eclipse-temurin:25-jdk-noble`, `ghcr.io/graalvm/graalvm-community:25`, `gcr.io/distroless/base-debian12:nonroot`); match the Java version of the project and update when it changes. Consider pinning by digest for reproducibility.
- No Docker `HEALTHCHECK` (distroless has no curl); probe `/actuator/health` from the orchestrator (liveness/readiness: `management.endpoint.health.probes.enabled=true`).
- Keep a `.dockerignore` (`target/`, `build/`, `.git`, `.env`, IDE files) so secrets and build output stay out of the context.
## Build and run
```bash
docker build -t my-spring-app .
docker run --rm -p 8080:8080 \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://host.docker.internal:5432/mydb \
-e SPRING_DATASOURCE_USERNAME=user -e SPRING_DATASOURCE_PASSWORD=password \
my-spring-app
```
Alternative without a Dockerfile: `./mvnw spring-boot:build-image` or `./gradlew bootBuildImage` (Cloud Native Buildpacks; add `-Pnative` for native with Maven, Gradle needs the `org.graalvm.buildtools.native` plugin). Verified on Boot 4.1.1 (web + actuator + validation + data-jpa, Java 25): Maven native build-image about 2m40, 282 MB image; Gradle `bootBuildImage` 2-4 min, image starts in 0.1-0.2 s and `/actuator/health` returns UP with native-build-tools 1.1.7 and 1.1.8. A native image that crashes at startup with `Invalid logger interface org.hibernate.validator.internal.util.logging.Log (implementation not found)` was seen once with Gradle but could not be reproduced; it matches [graalvm/native-build-tools#1017](https://github.com/graalvm/native-build-tools/issues/1017) (Gradle `collectReachabilityMetadata` overwrote the hibernate-validator metadata with the jboss-logging one, leaving a ~2 KB file). If it happens, check `unzip -l build/libs/*.jar | grep hibernate-validator.*reachability-metadata.json` (healthy is about 150 KB), upgrade `org.graalvm.buildtools.native` to the latest release and rebuild with `--no-build-cache`; always run the image and hit `/actuator/health`.
## Development
Use `spring-boot-docker-compose` with `compose.yaml` for dependencies (see [database.md](database.md)); do not containerize the application itself during development.
references/logging.md
# Logging
Spring Boot logs with Logback through SLF4J by default. Code must depend only on the SLF4J API (`org.slf4j.Logger`), never on Logback or `System.out`.
```java
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
logger.info("Order {} created for customer {}", order.id(), customerId);
logger.error("Payment failed for order {}", orderId, exception); // exception last, no placeholder
```
## Rules
- Parameterized messages, not string concatenation.
- Levels: `ERROR` = needs attention; `WARN` = recoverable anomaly; `INFO` = business events and lifecycle; `DEBUG`/`TRACE` = diagnostics (off in production).
- Never log secrets, tokens, passwords or personal data; log identifiers instead.
- Log an exception once, where it is handled, with the stack trace.
- Do not log inside tight loops.
## Configuration
```properties
logging.level.root=INFO
logging.level.com.example=INFO
# dev profile: logging.level.com.example=DEBUG
```
Prefer properties over a custom `logback-spring.xml`; add the XML only for needs properties cannot cover (appenders, rolling files).
## Structured logging (Spring Boot 3.4+)
For production and log aggregation, emit JSON without extra dependencies:
```properties
logging.structured.format.console=ecs
```
Other built-in formats: `logstash`, `gelf`. In containers, log to the console and let the platform collect it.
## Correlation
Use Micrometer Tracing / the `spring-observability` skill so trace and span ids appear in logs; use MDC for request-scoped values (`MDC.put("requestId", ...)`, always cleared in `finally`).
references/PROVENANCE.md
# Provenance
This skill and its references are adapted from **dr-jskill** by Julien Dubois (the contributor of this skill).
- Source: https://github.com/jdubois/dr-jskill
- Commit read: `29e71996048e4086bfa389268a3c6c2e49ea3f25`
- License of the source: Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
- The adapted material is contributed to this repository by its original author (Julien Dubois, sole copyright holder of dr-jskill) under this repository's MIT license, and remains available upstream under Apache-2.0. Keep the attribution and this notice with any redistribution of the adapted files.
## Files adapted
| Source file in dr-jskill | Adapted into |
|--------------------------|--------------|
| `SKILL.md` | `SKILL.md` (rewritten, shortened, Maven and Gradle) |
| `versions.json`, `scripts/lib/versions.mjs` | `references/versions.md` (documented procedure and fallback values; script not copied) |
| `references/CONFIGURATION.md` | `references/configuration.md` (condensed) |
| `references/DATABASE.md` | `references/database.md` (condensed; Flyway/Liquibase noted as optional) |
| `references/LOGGING.md` | `references/logging.md` (condensed) |
| `references/SECURITY.md` | `references/security.md` (condensed to basics) |
| `references/DOCKER.md`, `assets/Dockerfile`, `assets/Dockerfile-native` | `references/docker.md`, `assets/Dockerfile`, `assets/Dockerfile-native` (front-end build steps removed) |
| `references/SPRING-BOOT-4.md` | `references/spring-boot-4.md` (condensed) |
## Changes
Changes were made: content was condensed, front-end, Azure, Git, CRaC and AOT material was removed (deferred), Gradle support was added, and scripts were replaced by documented `curl` procedures against start.spring.io.
references/security.md
# Security basics
For anything beyond the basics (OAuth2/OIDC, JWT resource server, method security, CSRF/CORS details, testing security) use the `spring-security` skill. This page covers secure defaults when scaffolding.
## Dependencies
Add `security` at generation time (Boot 4: `spring-boot-starter-security`, plus `spring-boot-starter-security-test` in test scope if generated by start.spring.io). Adding it secures every endpoint by default.
## Minimal explicit configuration
```java
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**", "/actuator/info").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
```
- For local development, leave Boot's generated user and password encoder setup intact. Do not add a `PasswordEncoder` bean alone: Boot's generated password is then left unencoded and cannot authenticate with a delegating encoder.
- Deny by default: `anyRequest().authenticated()` last.
- Keep CSRF enabled for browser sessions; disable it only for stateless token APIs, deliberately.
- For production password authentication, configure a persistent user store and a matching `PasswordEncoder` together. Use `PasswordEncoderFactories.createDelegatingPasswordEncoder()` and store its encoded `{id}hash` values, never plain text.
- Do not use `{noop}` passwords or `InMemoryUserDetailsManager` demo users outside local experiments.
## Secure defaults checklist
- [ ] Secrets from environment/secret store, never committed (see [configuration.md](configuration.md))
- [ ] Expose only `health` and `info` over HTTP: `management.endpoints.web.exposure.include=health,info`
- [ ] Validate input (`@Valid`, Bean Validation) and use parameterized queries / Spring Data
- [ ] Error responses via RFC 9457 Problem Details: `spring.mvc.problemdetails.enabled=true`; no stack traces to clients (`server.error.include-stacktrace=never`)
- [ ] CORS configured explicitly with allowed origins, never `*` with credentials
- [ ] HTTPS terminated at the platform; `server.forward-headers-strategy=framework` behind a proxy
- [ ] Dependencies kept current; container runs as non-root (see [docker.md](docker.md))
references/spring-boot-4.md
# Spring Boot 3 vs 4 (when generating or editing code)
Detect the project's Spring Boot major first (see [versions.md](versions.md)); apply only the section that matches. Verify against the [Spring Boot 4.0 migration guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide).
## Baselines
| | Boot 3.x | Boot 4.x |
|--|----------|----------|
| Spring Framework | 6.x | 7.x |
| Java | 17+ | 17+ (25 recommended) |
| Jakarta EE | 10 | 11 |
| Jackson | 2 (`com.fasterxml.jackson.databind`) | 3 (`tools.jackson.databind`) |
| Testcontainers | 1.x | 2.x |
## Things that commonly break in Boot 4
1. **Modular starters.** Web is `spring-boot-starter-webmvc` (not `-web`), and each technology has a matching test starter (`spring-boot-starter-webmvc-test`, `spring-boot-starter-data-jpa-test`...). Generate with start.spring.io rather than guessing artifact names.
2. **Jackson 3.** Only API classes move (`tools.jackson.databind.ObjectMapper`); annotations stay in `com.fasterxml.jackson.annotation`.
3. **Test annotations moved**, for example `@WebMvcTest` is now `org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest`. `@MockitoBean` / `@MockitoSpyBean` replace `@MockBean` / `@SpyBean` (deprecated since 3.4 and removed in Boot 4; use the new ones everywhere Boot 3.4+ allows). See the `spring-boot-testing` skill.
4. **Testcontainers 2.x.** Artifacts are prefixed (`testcontainers-postgresql`, `testcontainers-junit-jupiter`) and classes moved (`org.testcontainers.postgresql.PostgreSQLContainer`). With Boot 3 keep the 1.x names.
5. **Main class for AOT/native.** If `process-aot` cannot find the main class, set `<start-class>` (Maven property) to the fully qualified `@SpringBootApplication` class.
6. **Make the generated `TestcontainersConfiguration` public** if tests live in sub-packages.
## Boot 3 projects
Stay on Boot 3 when the project is on Boot 3 (start.spring.io cannot generate new 3.x projects, see [versions.md](versions.md)): use `spring-boot-starter-web`, `org.testcontainers.containers.PostgreSQLContainer` (Testcontainers 1.x), `@MockBean`, Jackson 2. Do not migrate unless asked; Boot 3.5 → 4.0 is a separate, deliberate upgrade.
references/versions.md
# Version handling
## Rules
1. **Existing project:** read versions from `pom.xml` / `build.gradle(.kts)` (Spring Boot parent or plugin version, `java.version`/toolchain, `spring-boot-starter-test`). Keep them. Never bump implicitly.
2. **New project:** resolve at run time, fall back to the table below only when offline.
3. Never use `latest`, `LATEST` or `RELEASE` as a version, nor piped shell installers.
## Resolve the latest stable Spring Boot
```bash
curl -fsS -H 'Accept: application/json' https://start.spring.io/metadata/client \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(d['bootVersion']['default'], d['javaVersion']['default'])"
```
- `bootVersion.default` is the current stable release (it may carry a `.RELEASE` suffix such as `4.1.1.RELEASE`; strip it: pass `4.1.1`).
- Skip ids containing `SNAPSHOT`, `M` or `RC` (milestones/snapshots) unless the user asks for them.
- `javaVersion.default` is only the start.spring.io default. Pick the highest LTS the installed JDK supports (`java -version`), and make sure it is offered in `javaVersion.values`.
- Use the major line the user asks for. If they do not say, use the latest stable; see [spring-boot-4.md](spring-boot-4.md) for what changes between 3.x and 4.x.
- start.spring.io only generates Boot 4.0+ (verified: `bootVersion=3.5.x` returns HTTP 400 "compatibility range is >=4.0.0"). For a Boot 3.5 project, generate nothing: hand-write the build file with `spring-boot-starter-parent` 3.5.x (latest 3.5 on Maven Central, e.g. 3.5.16), `spring-boot-starter-web`, and `spring-boot-starter-test`, plus the Maven/Gradle wrapper. Creating the wrapper (`mvn wrapper:wrapper` / `gradle wrapper`) requires Maven or Gradle to be installed locally; if neither is available, ask the user to install one or choose Boot 4.
- Verify the choice exists on Maven Central: `curl -fsSI https://repo1.maven.org/maven2/org/springframework/boot/spring-boot/BOOT_VERSION/spring-boot-BOOT_VERSION.pom`.
## Fallback values (offline, reviewed 2026-10)
| Item | Value |
|------|-------|
| Spring Boot | 4.1.1 |
| Java | 25 (LTS) |
| PostgreSQL image | `postgres:18-alpine` |
| Testcontainers | managed by the Spring Boot BOM; do not pin unless overriding |
| JDK image | `eclipse-temurin:25-jdk-noble` |
| GraalVM image | `ghcr.io/graalvm/graalvm-community:25` |
These values age: when start.spring.io is reachable, prefer its answer, and tell the user which source was used.
## Dependencies managed by the BOM
Do not specify versions for Spring-managed dependencies (Hibernate, Jackson, Testcontainers, PostgreSQL driver, JUnit). Pin only what the BOM does not manage (for example a Maven plugin or a Docker image tag).
SKILL.md
---
name: create-spring-boot-java-project
description: 'Create a new Spring Boot Java project (Maven or Gradle) from start.spring.io with sensible defaults for configuration, PostgreSQL, logging, security, testing and Docker. Use when asked to scaffold, bootstrap or generate a Spring Boot application. Detects existing project versions and never upgrades implicitly.'
---
# Create a Spring Boot Java project
Scaffold a Spring Boot application with [start.spring.io](https://start.spring.io), then apply a small set of opinionated defaults. Out of scope for now: front-ends (Vue/React/Angular), cloud deployment (Azure), Git setup, CRaC and AOT images.
## Step 0: Detect before you create
- If the working directory already contains a `pom.xml` or `build.gradle(.kts)`, this is an **existing project**: do not regenerate it. Read its Spring Boot, Java and test-framework versions and follow them. **Never upgrade versions implicitly**; propose an upgrade and wait for the user's decision.
- Check the tools: `java -version` (JDK matching the chosen Java version), `docker --version` (needed for Testcontainers and Compose). On the default path (current Boot via start.spring.io) Maven/Gradle need not be installed: the generated wrapper is used. The Boot 3.5 fallback needs Maven or Gradle installed once to create the wrapper (see [versions.md](references/versions.md)).
- Ask only for what is missing: project name, group id, package, build tool (default **Maven**), dependencies (default: web, actuator, validation, devtools; add `data-jpa`, `postgresql`, `security`, `docker-compose`, `testcontainers` when a database or auth is needed).
## Step 1: Resolve versions
Follow [references/versions.md](references/versions.md): query `https://start.spring.io/metadata/client` at run time for the latest stable Spring Boot release, fall back to the documented pinned values if offline. Do not hardcode `latest` anywhere.
## Step 2: Generate the project
Use the start.spring.io HTTP API with explicit, pinned parameters (no piped installers). Replace the values in capitals.
Maven:
```bash
curl -fsS https://start.spring.io/starter.zip \
-d type=maven-project -d language=java -d packaging=jar \
-d bootVersion=BOOT_VERSION -d javaVersion=JAVA_VERSION \
-d groupId=com.example -d artifactId=demo -d name=demo \
-d packageName=com.example.demo -d baseDir=demo \
-d dependencies=web,actuator,validation,devtools \
-o demo.zip && unzip -q demo.zip && rm demo.zip
```
Gradle: same command with `-d type=gradle-project` (Groovy DSL) or `-d type=gradle-project-kotlin` (Kotlin DSL; the language stays `java`).
Inspect the result (`ls demo`, read the build file) and make sure the wrapper (`mvnw` or `gradlew`) is executable. Add `-d dependencies=...,data-jpa,postgresql,docker-compose,testcontainers` for a database project.
## Step 3: Apply the defaults
| Area | What to do | Reference |
|------|-----------|-----------|
| Boot 3 vs 4 differences | Starter names, Jackson 3, test modules | [references/spring-boot-4.md](references/spring-boot-4.md) |
| Configuration | `application.properties` or `.yml` (match the project), profiles, `@ConfigurationProperties`, secrets via environment | [references/configuration.md](references/configuration.md) |
| Database | PostgreSQL, Hibernate settings, Compose for dev, Testcontainers for tests | [references/database.md](references/database.md) |
| Logging | SLF4J, parameterized messages, levels, structured logging | [references/logging.md](references/logging.md) |
| Security | Spring Security basics, secure defaults, Problem Details | [references/security.md](references/security.md) |
| Docker | JVM image and GraalVM native image Dockerfiles | [references/docker.md](references/docker.md) |
| Testing | See the `spring-boot-testing` skill (slices, Testcontainers, Failsafe) | `skills/spring-boot-testing` |
| Best practices | See the `java-springboot` skill and `instructions/springboot.instructions.md` | `skills/java-springboot` |
Keep dotfiles minimal: a `.gitignore` covering `target/`, `build/`, `.gradle/`, IDE folders and `.env`; never commit `.env` (commit a `.env.sample`).
## Step 4: Verify
1. Build and test: `./mvnw verify` (Maven) or `./gradlew build` (Gradle).
2. Run: `./mvnw spring-boot:run` or `./gradlew bootRun`, then `curl -fsS localhost:8080/actuator/health` must return `{"status":"UP"}`.
3. Report the chosen versions and anything that was skipped or deferred.
## Provenance
Adapted from the Apache-2.0 skill [jdubois/dr-jskill](https://github.com/jdubois/dr-jskill); see [references/PROVENANCE.md](references/PROVENANCE.md).