---
name: spring-boot-service-engineering
description: "Engineering skill: Spring Boot service — how to build and ship a api / service with Spring Boot, Java, JUnit 5 + AssertJ. Use when writing, reviewing, testing or releasing code in this stack, and when turning a Celestial design or experience skill into code."
type: engineering
stack: api
core: "1.0"
checked: 2026-10-26
---
# Spring Boot service
A Java REST service on Spring Boot with layered packages by feature, validated DTOs, Flyway migrations and integration tests against real dependencies.
For business-critical backends: payments, orders, accounts, integrations with existing enterprise systems. Packaged as a container image and run on a container orchestrator, with health and metrics endpoints exposed through Actuator.
## Stack
- **Language:** Java 21+ (LTS) — Records, sealed types, pattern matching and virtual threads.
- **Framework:** Spring Boot 3.4+ — Auto-configuration, Spring MVC, Spring Security and Actuator.
- **State:** Stateless requests; JWT or opaque tokens validated per request — Horizontal scaling without sticky sessions.
- **Data:** PostgreSQL with Spring Data JPA + Flyway Postgres 16+ · Flyway ^10 — Repositories for common queries; SQL migrations versioned in the repo.
- **Build:** Gradle (Kotlin DSL) ^8.10 — Incremental builds, version catalogue and bootJar packaging.
- **Hosting:** Container orchestrator with health probes — Liveness and readiness come from Actuator; rolling updates by default.
- **Package manager:** Gradle version catalogue gradle/libs.versions.toml — Every dependency version in one file.
- **Testing:** JUnit 5 + AssertJ JUnit ^5.11 — Standard test runner with fluent assertions.
- **Testing:** Spring Boot Test + MockMvc with Boot — Slice tests for web and data layers.
- **Testing:** Testcontainers ^1.20 — Integration tests against real Postgres and brokers.
## Commands
- dev: `./gradlew bootRun --args=--spring.profiles.active=local`
- e2e: `./gradlew integrationTest`
- lint: `./gradlew spotlessCheck checkstyleMain`
- test: `./gradlew test`
- build: `./gradlew bootJar`
- deploy: `./gradlew bootBuildImage`
- install: `./gradlew dependencies`
## Structure
- `src/main/java//Application.java` — Spring Boot entry point
- `src/main/java///` — Package per feature: Controller, Service, Repository, entities and DTO records
- `src/main/java//common/` — Security config, exception handlers, shared value types
- `src/main/resources/application.yml` — Defaults; secrets and environment values come from environment variables
- `src/main/resources/db/migration/` — Flyway migrations V__description.sql
- `src/test/java/` — Unit, slice and Testcontainers integration tests mirroring main packages
## Naming
- Files: PascalCase.java, one public type per file
- Tests: Test.java for unit; IT.java for integration
- Branches: type/short-description, e.g. feat/payout-schedule
## From design and experience skills
This stack has no UI; design skills do not apply.
When a Celestial experience skill is in use, give each journey one end-to-end test (happy path plus its error and empty states) and name analytics events `object_action` from its measures.
## Conventions
### Code
- Prefer small, pure functions and explicit types at module boundaries. _(Easier to test, review and change.)_
- Organise by feature package, not by layer; classes are package-private unless another feature needs them. _(Boundaries are enforced by the compiler.)_
- Use constructor injection only; no field injection with @Autowired. _(Dependencies are explicit and classes are testable without Spring.)_
### Components
- Components read design tokens; no raw hex values, pixel sizes or font names in component code. _(One token change restyles the product.)_
### Data
- Analytics events are object_action in snake_case and come from the experience skill’s measures. _(Names stay consistent across teams and tools.)_
- Controllers accept and return DTO records annotated with Bean Validation and @Valid; JPA entities never cross the controller boundary. _(Prevents over-posting and lazy-loading surprises during serialisation.)_
### Errors
- Every async action has loading, empty, error and success states, matching the experience skill. _(Unhandled states are where users get stuck.)_
- Return RFC 9457 problem details from one @RestControllerAdvice; log the exception once with a correlation id. _(Consistent errors for clients and traceable logs for operators.)_
### Security
- Secrets never live in the repository; read them from the environment or a secret store. _(A leaked key outlives the commit that removed it.)_
- Validate every input on the server, even when the client already did. _(Clients can be bypassed.)_
- Web pages ship a Content-Security-Policy, HSTS, X-Frame-Options and nosniff. _(Cheap protection against the most common attacks.)_
- Configure a SecurityFilterChain that denies by default and expose only /actuator/health publicly. _(New endpoints are protected until someone opens them on purpose.)_
### Performance
- Set open-in-view to false, define fetch plans per query, and keep @Transactional on service methods only. _(Avoids hidden queries and long-held connections.)_
### Accessibility
- Use native elements first; add ARIA only when no native element fits. _(Native controls bring keyboard and screen-reader support for free.)_
## Do
- Write Flyway migrations that the previous release can run against
- Expose metrics through Micrometer and trace with OpenTelemetry
- Use @WebMvcTest and @DataJpaTest slices for fast feedback
## Don't
- Use ddl-auto=update outside local development
- Catch Exception and swallow it
- Store secrets in application.yml
## Quality gates
| Gate | Tool | Threshold | When |
|---|---|---|---|
| Lint and types | The stack’s linter and type checker | 0 errors | Every pull request |
| Unit and component tests | The stack’s test runner | All pass; every design-skill component state covered | Every pull request |
| Performance budget | Gatling or k6 load test against the preview environment | p95 ≤ 250ms and p99 ≤ 600ms at target RPS; error rate < 0.1% | Every pull request touching an endpoint or query |
| Dependency and secret scan | Dependency audit + secret scanner | No high or critical findings; no secrets | Every pull request and nightly |
| Preview deploy | Hosting preview environment | A working preview link on the pull request | Every pull request |
| Integration tests | Testcontainers suite | All pass against real Postgres | Every pull request |
## Workflow
- Branching: Trunk-based: short-lived branches off main, squash-merged within two days.
**Review checklist**
- Correctness: does it do what the issue asks, including edge cases?
- Readability: could a new teammate change it safely?
- Architecture: is it in the right place, with the right boundaries?
- Security: inputs, secrets, permissions, dependencies
- Performance: budgets, queries, bundle size
- Tests fail when the code is broken (flip a condition and re-run)
- One approving review; two for security config, migrations or money movement
- Reviewer checks the migration against the running schema
**Definition of done**
- Acceptance criteria met and demoed on the preview link
- Tests added for new behaviour and every state
- Accessibility and performance gates green
- Analytics events named from the experience skill
- Docs or changelog updated when behaviour changes
- DTOs validated and documented in the OpenAPI spec
- Integration test covers success, validation failure and forbidden access
**Release**
- Small pull requests (aim for under 400 changed lines)
- CI green before review
- Squash-merge to main; main is always deployable
- Every release has a written rollback path
- Merge to main builds an image; Flyway migrates on startup of the first new pod
- Readiness probe gates traffic until migration and warm-up finish
**Incidents**
- Roll back to the previous image; Flyway migrations stay forward-only, fixes ship as new migrations
- Blameless note within 48 hours
## Overrides of the engineering core
- Journey tests: Replaced by MockMvc API tests plus Testcontainers integration tests — No UI in this stack; the consumer-facing journey is the API contract.
- Accessibility: Not applicable — No UI in this stack; accessibility is owned by the clients that call it.
- components: Not applicable — No design-skill components or tokens in an API service.
- Performance budget: Replaced by server latency targets: p95 ≤ 250ms, p99 ≤ 600ms — Web Vitals and bundle size do not apply to an API.
## Examples
### Validated controller (`src/main/java/com/example/orders/OrderController.java`)
```java
package com.example.orders;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/orders")
class OrderController {
record CreateOrder(@NotNull UUID productId, @Min(1) @Max(100) int quantity) {}
record OrderView(UUID id, String status, long totalMinor) {}
private final OrderService orders;
OrderController(OrderService orders) { this.orders = orders; }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
OrderView create(@AuthenticationPrincipal(expression = "id") UUID userId,
@Valid @RequestBody CreateOrder body) {
return orders.create(userId, body.productId(), body.quantity());
}
}
```
## Further reading
- [Spring Boot documentation](https://docs.spring.io/spring-boot/) — Auto-configuration, Actuator, testing slices and packaging.
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/) — Concrete guidance on input validation, authentication and session handling.
- [OpenTelemetry documentation](https://opentelemetry.io/docs/) — Instrumenting traces, metrics and logs so latency budgets can be measured.