Spring Boot 4.1 / Java 25 service created from spring-template. Replace this paragraph with one sentence about
what the service does; everything below stays true for every project built from the template.
Core is one Spring service (services/app) + Postgres; everything else is a brick (bricks/, make bricks).
| Command | What | Time |
|---|---|---|
make run |
app on :8080, Postgres from compose.yml (GRADLE_SERVICE= picks the Gradle project, default app) |
20 s |
make test |
unit tests, no Docker | 10 s |
make itest |
integration tests, Testcontainers, one Postgres per fork | 1 min |
make build |
everything CI's build job runs: Spotless, unit, integration, JaCoCo gate 60 % |
1–2 min |
make test-scripts |
bash tests under scripts/tests |
5 s |
make format |
spotlessApply; run before every commit that touches Java or *.gradle.kts |
|
make bricks |
list bricks, [x] when enabled |
|
make add BRICK=<name> / make remove BRICK=<name> |
wire a brick in / take it out | |
make release-name |
version the next push to main would release |
The tree is clean at HEAD (build, tests, Spotless): any failure after your change is yours. make run/test/
build call make setup first, which installs the commit-msg hook.
- One Hetzner VPS running
deploy/compose.prod.yml: Caddy (TLS, only published ports) → 2appreplicas → Postgres (loopback only), plus Watchtower. Enabled bricks add their services through composeinclude:. - Push to
main= deploy.ci.ymlbuilds and tests, packagesghcr.io/<owner>/<repo>:{latest,X.Y.Z,X.Y,sha-…}, tagsvX.Y.Z, publishes a GitHub Release, and waits until${PRODUCTION_URL}/versionreports the new revision from every replica. Watchtower on the VPS pullslatestwithin 60 s and restarts replicas one at a time. Push → live ≈ 5–6 min. Nobody SSHes for app releases; enabling a brick or changing compose needsgit pull && make prod-upon the host (docs/deployment.md). - Rollback on the VPS:
make prod-rollback TAG=<previous version>. Schema is not rolled back: changesets must work with the previous release (expand/contract;docs/deployment.md§5). MANAGEMENT_PORT=8081in prod: actuator is never published or proxied./versionis the only public build info.
Subjects: <type>(<scope>)!: <imperative summary> with type ∈ feat fix perf refactor docs test build ci chore style revert. feat → minor, !/BREAKING CHANGE: → major (only when the user says so), rest → patch. Body
explains why. The hook and CI reject anything else. Details: .claude/skills/release/SKILL.md.
scripts/vps.sh (needs VPS_SSH in .env): make vps-status, make vps-logs SERVICE=app,
make vps-psql SQL="select …", make vps-datagrip. Credentials are read from the VPS per call and never stored
locally. Read-only unless the user asked for a specific change; never fix schema by hand. Skill:
.claude/skills/vps-db/SKILL.md.
.bricks lists enabled bricks. make add BRICK=<name> wires bricks/<name>/ in (compose include, env blocks, skill,
lockfile; its Java is a Gradle subproject :bricks:<name> while enabled); make remove reverses it. Read bricks/<name>/README.md before touching a
brick, bricks/README.md before authoring one. Skill: .claude/skills/bricks/SKILL.md. Observability (Grafana,
alerts, dashboard) and backups are bricks; their invariants live in their READMEs.
- Service code lives under
services/app(Gradle project:app;make run/dev/locktakeGRADLE_SERVICE=). Layers:controller→service→repository;job(schedulers,*Scheduler) may call services. Enforced byArchitectureTest, including constructor injection only, noSystem.out,@Scheduledonly underjob. - Liquibase owns the schema (
db/changelog/changes/*.yaml,includeAll); Hibernate validates. Boot 4 needsspring-boot-liquibaseon the classpath or nothing runs — it is there, do not remove it. - Config idiom
${ENV_VAR:default}inapplication.yml; typed access via@ConfigurationPropertiesrecords +@EnableConfigurationProperties(seeAppInfoProperties). @Slf4jonly. Secrets never reach logs:SecretRedactor, the redacting Feign logger andRequestResponseLoggingFilter(DEBUG only) already cover HTTP; useSecretRedactor.redactfor anything else.- Errors: throw
ApplicationException(status, message);GlobalExceptionHandlerrenders RFC 9457. Client mistakes (404/405/400) are quiet; only unexpected exceptions log at ERROR. - Feign clients under
client/, wrapped in a circuit breaker with a fallback class. InjectClock, neverInstant.now()directly. - Tests: unit under
unit/(concurrent, no Spring unless@WebMvcTest), integration underintegration/extendingAbstractIntegrationTest(constructor injection, MockMvc). Test data viasupport.Fixtures(Instancio). - Bash lives in
scripts/, tested byscripts/tests/*.test.sh; every script hasset -euo pipefailand a header comment that doubles as--help.
DOCKER_API_VERSION: "1.44"on Watchtower. Remove it and Docker 25+ rejects Watchtower's API calls: no deploys, healthy-looking container.APP_REPLICAS × DB_POOL_SIZE < max_connections (50).stop_grace_period(45 s) >SPRING_LIFECYCLE_TIMEOUT_PER_SHUTDOWN_PHASE(30 s) <WATCHTOWER_TIMEOUT(60 s).deploy/.env.prodrequired values carry:?guards; keep them when adding variables.- Brick partials of core services live in
override*.yml; the Makefile is the only place that builds thedocker compose -f …list (make print-prod).
Brainstorm → plan → TDD → verify → commit (superpowers skills). Specs and plans live in
docs/superpowers/{specs,plans}/YYYY-MM-DD-*.md. Verify with the command, read the output, then claim.