A concrete realization of the general guide for Java 21 + Spring Boot 3.x + Gradle (Maven equivalents apply). The objective: encircle agent velocity with the compiler, ArchUnit, PIT mutation testing, and automated CI.
1. Recommended Stack
| Domain | Tooling |
|---|---|
| Build System | Gradle (Kotlin DSL) + Version Catalog, Dependency Locking (--write-locks) |
| Code Formatting | Spotless (google-java-format or Palantir Java Format) |
| Static Analysis | Error Prone + NullAway, SpotBugs, Checkstyle (lightweight rules), SonarQube |
| Testing Frameworks | JUnit 5, AssertJ, Mockito (minimal), Testcontainers, @DataJpaTest, @WebMvcTest |
| Architecture Enforcement | ArchUnit, Spring Modulith (ApplicationModules.verify()) |
| Mutation Testing | PIT (Pitest) + pitest-junit5-plugin |
| Property-Based Testing | jqwik |
| Contract Testing | Spring Cloud Contract or Pact |
| Code Coverage | JaCoCo (diff coverage enforced via diff-cover or Sonar Quality Gate) |
| Security Auditing | CodeQL / Semgrep, OWASP Dependency-Check or OSV-Scanner, gitleaks |
| Database Migrations | Flyway (agents must never modify historical migration files) |
| Automated Refactoring | OpenRewrite (relegate deterministic transformations to recipes, not LLMs) |
2. Repository Structure (Modular Monolith)
src/main/java/com/acme/orders/
OrdersModule/ <- package-private module entry point & API boundaries
domain/ <- pure Java business logic, ZERO Spring/Jakarta/ORM imports
application/ <- use cases, port interfaces, orchestrators
adapter/
in/web/ <- @RestController, request/response DTO records
out/persistence/ <- JPA entities, Spring Data repository implementations
config/ <- framework configuration beans
docs/adr, docs/specs
.claude/skills, .claude/settings.json
AGENTS.md build.gradle.kts
A hexagonal (ports and adapters) layout provides an ideal boundary for AI systems: component responsibilities are unambiguous, and boundary violations can be enforced directly by automated ArchUnit assertions.
3. AGENTS.md (Java & Spring Boot Template)
# Orders Service
Java 21, Spring Boot 3.3, Gradle, PostgreSQL, Flyway.
## Core Commands
- Fast check: `./gradlew spotlessApply check -x pitest`
- Full quality gate: `./gradlew check pitest`
- Single test target: `./gradlew test --tests "*OrderServiceTest"`
- Integration suite: `./gradlew integrationTest` (requires Docker daemon)
## Architectural Invariants (Enforced via ArchUnit)
- The `domain` package must never import Spring, JPA/Hibernate, or Jackson packages
- REST controllers must never depend directly on persistence repositories; invoke use cases
- Rely exclusively on constructor injection; field injection via `@Autowired` is prohibited
- Entities must never be returned across API boundaries; map explicitly to immutable record DTOs
## Invariants & Prohibitions
- Never modify past Flyway migration files; always append a new `V__` script
- Never introduce `@Disabled`, `@Ignore`, or delete tests to green a broken build
- Never weaken assertion checks to make tests pass
- Ask before adding third-party dependencies in `build.gradle.kts`
- Prohibit `System.out.println`, `e.printStackTrace()`, or empty catch blocks
## Coding Style & Conventions
- Prefer `record`, sealed interfaces, and `Optional` return types; never return `null`
- Test naming convention: `should_<expectedResult>_when_<condition>`; use AssertJ assertions
- Prefer Testcontainers or in-memory test doubles over heavy Mockito mocking
## Definition of Done
`check` and `pitest` tasks execute cleanly; all spec acceptance criteria are validated by automated tests; ADR authored if architectural decisions changed.
4. Architectural Rules (ArchUnit)
Add the ArchUnit JUnit 5 dependency to build.gradle.kts:
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
Create an ArchitectureTest.java suite to enforce layer boundaries:
@AnalyzeClasses(packages = "com.acme", importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureTest {
@ArchTest
static final ArchRule layersAreRespected = layeredArchitecture()
.consideringOnlyDependenciesInLayers()
.layer("Web").definedBy("..adapter.in.web..")
.layer("Application").definedBy("..application..")
.layer("Domain").definedBy("..domain..")
.layer("Persistence").definedBy("..adapter.out.persistence..")
.whereLayer("Web").mayNotBeAccessedByAnyLayer()
.whereLayer("Persistence").mayNotBeAccessedByAnyLayer()
.whereLayer("Application").mayOnlyBeAccessedByLayers("Web", "Persistence")
.whereLayer("Domain").mayOnlyBeAccessedByLayers("Application", "Web", "Persistence");
@ArchTest
static final ArchRule domainIsPure = noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework..", "jakarta.persistence..", "com.fasterxml.jackson..")
.because("The domain layer must remain agnostic of frameworks and persistence technologies");
@ArchTest
static final ArchRule controllersNeverAccessRepositories = noClasses()
.that().areAnnotatedWith(RestController.class)
.should().dependOnClassesThat().areAssignableTo(Repository.class)
.because("Controllers must interact with domain use cases, not persistence layers");
@ArchTest
static final ArchRule noFieldInjection = noFields()
.should().beAnnotatedWith(Autowired.class)
.because("Constructor injection ensures immutability and testability");
@ArchTest
static final ArchRule noPackageCycles = slices().matching("com.acme.(*)..")
.should().beFreeOfCycles();
@ArchTest
static final ArchRule noStandardStreams = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;
@ArchTest
static final ArchRule noGenericExceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
@ArchTest
static final ArchRule transactionalBelongsToApplication = methods()
.that().areAnnotatedWith(Transactional.class)
.should().beDeclaredInClassesThat().resideInAPackage("..application..")
.because("Transaction boundaries must be orchestrated within the application layer");
}
Implementation Tips:
- For brownfield codebases with existing violations, use
FreezingArchRule.freeze(rule)to establish a baseline; new violations will fail the build immediately. - The
.because(...)string gives autonomous agents clear actionable guidance on how to fix violations. - For modular monoliths, verify module boundaries using Spring Modulith:
ApplicationModules.of(App.class).verify();.
5. Mutation Testing (PIT / Pitest)
Configure PIT in build.gradle.kts:
plugins {
id("info.solidsoft.pitest") version "1.15.0"
}
pitest {
junit5PluginVersion.set("1.2.1")
pitestVersion.set("1.17.0")
targetClasses.set(setOf("com.acme.*.domain.*", "com.acme.*.application.*"))
excludedClasses.set(setOf("*Config", "*Application", "*Dto", "*Entity"))
threads.set(Runtime.getRuntime().availableProcessors())
mutators.set(setOf("STRONGER"))
mutationThreshold.set(75) // Target 80+ for core business modules
coverageThreshold.set(80)
outputFormats.set(setOf("HTML", "XML"))
timestampedReports.set(false)
withHistory.set(true) // Incremental: unmutated code is skipped
avoidCallsTo.set(setOf("org.slf4j", "java.util.logging"))
}
Execute on changed code during PRs using pitest-git-plugin:
./gradlew pitest -Pfeatures="+GIT(from[origin/main])"
Full mutation suites run during nightly CI. Focus mutation testing on domain and application layers; mutators applied to web controllers, boilerplate DTOs, or configuration classes produce unhelpful noise.
Survivor Triage Loop: Pass surviving mutants from build/reports/pitest/mutations.xml directly to an AI agent: "Author behavioral tests that kill these surviving mutants; do not modify production code."
6. Testing Practices
- Testcontainers for PostgreSQL: Eliminate the risk of "green in H2, broken in production PostgreSQL" by testing against real database engines.
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
class OrderRepositoryIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
}
- Targeted Test Slices: Leverage Spring's test slice annotations (
@WebMvcTestfor controllers,@DataJpaTestfor data layers). Avoid heavy@SpringBootTestsweeps across every test file to keep verification fast. - Property-Based Testing with jqwik: Validate invariants with generated inputs:
@Property
void discountedPriceMustNeverBeNegative(
@ForAll @Positive BigDecimal price,
@ForAll @IntRange(min = 0, max = 100) int discountRate) {
assertThat(PriceCalculator.applyDiscount(price, discountRate))
.isGreaterThanOrEqualTo(BigDecimal.ZERO);
}
- Deterministic Time: Always inject a
Clockbean into domain services; banLocalDateTime.now()invocations in application logic to avoid flaky assertions. - Mock at External Boundaries: Avoid mocking internal service classes; excessive mocks break upon refactoring and produce false positives under mutation testing.
- API Contract Verification: Validate API contracts using
openapi-diffin CI pipelines to prevent accidental backwards-incompatible schema changes.
7. Static Quality Enforcement (Error Prone & NullAway)
Configure Gradle to eliminate nullability defects and unsafe type casts at compile time:
plugins {
id("net.ltgt.errorprone") version "4.0.1"
}
dependencies {
errorprone("com.google.errorprone:error_prone_core:2.30.0")
errorprone("com.uber.nullaway:nullaway:0.11.2")
}
tasks.withType<JavaCompile> {
options.errorprone {
check("NullAway", CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", "com.acme")
}
options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror"))
}
Pairing -Werror with NullAway ensures that null dereference risks and raw type usages are rejected by the compiler before tests or code reviews take place.
8. Claude Code Integration Hooks
Configure local feedback loops in .claude/settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": ".claude/hooks/guard-bash.sh" }]
}],
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": ".claude/hooks/post-edit.sh" }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": ".claude/hooks/on-stop.sh" }]
}]
}
}
.claude/hooks/guard-bash.sh (Block destructive commands):
#!/usr/bin/env bash
cmd=$(jq -r '.tool_input.command' < /dev/stdin)
if echo "$cmd" | grep -Eq 'git push.*--force|rm -rf /|DROP TABLE|flyway clean'; then
echo "Blocked prohibited command: $cmd" >&2; exit 2
fi
.claude/hooks/post-edit.sh (Automated formatting, compilation, and test-skip detection):
#!/usr/bin/env bash
f=$(jq -r '.tool_input.file_path' < /dev/stdin)
case "$f" in
*.java) ./gradlew -q spotlessApply compileJava compileTestJava || exit 2 ;;
esac
if grep -q '@Disabled' "$f" 2>/dev/null; then
echo "@Disabled is prohibited: fix the test instead of ignoring it" >&2; exit 2
fi
.claude/hooks/on-stop.sh (Prevent finishing while checks are failing):
#!/usr/bin/env bash
./gradlew -q check -x pitest 2>&1 | tail -30 >&2 || exit 2
9. Skill Templates
.claude/skills/add-endpoint/SKILL.md:
---
name: add-endpoint
description: Use when adding a new REST endpoint. Produces controller, use case, DTO records, tests, and OpenAPI spec updates following hexagonal architecture.
---
1. Read the relevant spec in docs/specs; list acceptance criteria.
2. Author failing WebMvcTest and use case unit tests first (TDD).
3. Place use case and port under application/; place controller and immutable record DTO under adapter/in/web/.
4. Never return JPA entities directly over the wire; format API errors with ProblemDetail (RFC 9457).
5. Verify `./gradlew check` and `ArchitectureTest` pass cleanly before finishing.
6. Commit in atomic slices: tests, implementation, documentation.
.claude/skills/mutation-triage/SKILL.md:
---
name: mutation-triage
description: Analyzes PIT surviving mutants and writes behavioral tests to kill them.
---
1. Run `./gradlew pitest` and parse build/reports/pitest/mutations.xml.
2. Group surviving mutants by class; identify and document equivalent mutants.
3. For each actionable survivor, author a test that verifies the missing behavior (not internal implementation details).
4. Do not modify production code. Re-run pitest and report the updated mutation score.
Recommended additional skills: flyway-migration (expand/contract migrations), openrewrite-upgrade (automated Spring Boot framework upgrades), add-arch-rule, and security-review.
10. Continuous Integration Pipeline (GitHub Actions)
name: ci
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: 21, cache: gradle }
- run: ./gradlew spotlessCheck check # Unit tests, ArchUnit, Error Prone
- run: ./gradlew integrationTest # Testcontainers suites
- run: ./gradlew pitest -Pfeatures="+GIT(from[origin/main])"
- uses: github/codeql-action/init@v3
with: { languages: java }
- uses: github/codeql-action/analyze@v3
- run: ./gradlew dependencyCheckAnalyze # Or OSV-Scanner
- uses: gitleaks/gitleaks-action@v2
Nightly workflows run comprehensive mutation suites, stress testing, and dependency updates. All verify jobs are strictly required in GitHub branch protection rules.
11. Spring-Specific AI Pitfalls and Remedies
| AI Pitfall | Automated Remedy |
|---|---|
Field injection with @Autowired, circular bean dependencies | ArchUnit rules enforcing constructor injection |
| Returning JPA entities from controllers, lazy-loading exceptions | DTO record requirement enforced via ArchUnit and unit tests |
Misplaced @Transactional annotations, self-invocation bypasses | ArchUnit rules restricting transactional methods to application layer |
| N+1 query cascades in relational queries | @DataJpaTest with Hibernate query count assertions / datasource-proxy |
Outdated Spring Boot 2 patterns (e.g., WebSecurityConfigurerAdapter) | Target version in AGENTS.md, -Werror, OpenRewrite recipes |
Permissive security configuration (permitAll() sprawl) | Security slice tests (@WithMockUser) and strict peer review |
| Editing historical Flyway migrations | Pre-commit hook + CI checksum validation |
| Green mock tests that fail against real database engines | Testcontainers integration suites + PIT mutation testing |
| Excessive Lombok boilerplate or runtime reflection magic | Java 21 records and sealed interfaces; explicit AGENTS.md rules |
Unchecked Optional.get() and unexpected null returns | NullAway + Error Prone compiler enforcement |
12. Phased Adoption Roadmap
- Foundation: Establish
AGENTS.md, configure./gradlew check, Spotless, and Error Prone with-Werror. - Architecture: Introduce ArchUnit with initial 6–8 layer rules, freezing legacy violations where necessary.
- Continuous Verification: Standardize Testcontainers, GitHub Actions CI, CodeQL, and gitleaks scans.
- Mutation Quality Gates: Deploy PIT on domain and application layers, stepping thresholds from 60% toward 80%.
- Agent Ergonomics: Implement Claude Code local hooks, 3–4 tailored task skills, and dedicated reviewer subagents.
- Advanced Testing: Introduce property-based testing with jqwik, contract testing, and Spring Modulith verifications.
- Metrics & Feedback: Track change failure rates, mutation score trends, and average PR diff sizes to continually calibrate guardrails.
Netology