A small REST service for logging BJJ techniques, built as a focused, production-shaped example rather than a tutorial CRUD demo. Focus not on a domain but on every layer is wired the way a real service should be: versioned schema, real-database tests, standardized error contracts, and Kubernetes-ready health probes.
- Java 21 (virtual threads enabled)
- Spring Boot 3.5 — Web, Data JPA, Validation, Actuator
- PostgreSQL with Flyway for schema migrations
- Testcontainers for integration tests against a real database
- Maven (wrapper included)
- Docker — Postgres is started automatically via Spring Boot's Compose support
A CRUD-style API over a technique resource (name, position, type, notes).
| Method | Path | Description |
|---|---|---|
GET |
/api/techniques |
List all techniques |
GET |
/api/techniques/{id} |
Fetch one (404 as RFC 7807 problem+json if missing) |
POST |
/api/techniques |
Create one (validated request body) |
Operational endpoints (Actuator): /actuator/health, /actuator/health/liveness, /actuator/health/readiness, /actuator/info, /actuator/metrics.
Prerequisites: Java 21 and a running Docker daemon. Nothing else — you do not install Postgres yourself.
./mvnw spring-boot:runOn startup, Spring Boot reads compose.yaml, starts a PostgreSQL container, wires the datasource to it automatically (@ServiceConnection), and runs Flyway migrations. The app comes up on http://localhost:8080.
# create a technique
curl -s localhost:8080/api/techniques -H 'Content-Type: application/json' \
-d '{"name":"Triangle from guard","position":"closed guard","type":"submission"}'
# list them
curl -s localhost:8080/api/techniques | jq
# a missing id returns an RFC 7807 problem document
curl -s localhost:8080/api/techniques/999
# health
curl -s localhost:8080/actuator/health./mvnw testIntegration tests use Testcontainers, so a Docker daemon must be running. Each run spins up a fresh PostgreSQL container, applies the Flyway migrations against it, and exercises the API end to end — no in-memory database substitution.
These are deliberate and are the actual point of the project:
- Flyway owns the schema, Hibernate validates it.
ddl-auto: validatemeans Hibernate never mutates the database; all schema changes are versioned, reviewable SQL migrations underdb/migration. The app fails fast at startup if the schema and entities disagree. - Integration tests run against real Postgres, not H2. Testcontainers removes the friction that usually pushes people toward an in-memory fake, so tests catch dialect- and constraint-level issues that H2 would hide.
- Immutable DTOs at the edges, mutable entity in the middle. Request/response objects are
recordtypes (validated, immutable); the JPA entity is mutable because Hibernate requires it. The API contract is decoupled from the database schema. - Standardized errors via RFC 7807. A single
@RestControllerAdvicemaps exceptions toProblemDetailresponses, so every error in the service has one consistent, documented shape. - Kubernetes-ready operations. Actuator liveness/readiness probes are enabled and map directly onto Kubernetes probe semantics;
open-in-viewis disabled to keep transaction boundaries explicit and avoid holding DB connections through response serialization. - Virtual threads. Requests run on virtual threads (
spring.threads.virtual.enabled), giving the blocking, readable MVC model async-like scalability under I/O-bound load.
src/main/java/com/bjj/techlog/
Technique.java # JPA entity
TechniqueRequest.java # request DTO (record, validated)
TechniqueResponse.java # response DTO (record)
TechniqueRepository.java # Spring Data JPA repository
TechniqueController.java # REST endpoints
ApiExceptionHandler.java # RFC 7807 error mapping
src/main/resources/
application.yml
db/migration/V1__create_technique.sql
compose.yaml # Postgres for local dev (auto-started)