Java has more implicit convention packed into it than almost any other mainstream language. Dependency injection style, layering, exception handling, transaction boundaries — none of it is enforced by the compiler, all of it is enforced by code review, and most of it lives in tribal knowledge that never made it into a README.
That’s a problem for Claude Code, because without a CLAUDE.md, an agent working on a Spring Boot project will:
- Reach for field injection (
@Autowiredon a field) instead of constructor injection, because that’s what most Stack Overflow answers from a decade ago still show - Return JPA entities directly from
@RestControllermethods instead of mapping to DTOs, leaking lazy-loading exceptions and internal schema details into the API - Skip
@ControllerAdviceand let exceptions bubble up as raw 500s with stack traces - Guess between Maven and Gradle build commands, or run both and produce two conflicting build files
- Miss transaction boundaries on service methods that touch multiple repositories
None of these break the build. They compile, Spring Boot starts, the happy path works in a manual test. They just aren’t what a senior Spring engineer would ship — and by the time it’s in code review, the pattern has usually already been copied into three other files.
A CLAUDE.md closes that gap before the first pull request.
Why Spring Boot Needs More Guidance Than Most Frameworks
Spring’s dependency injection container means there are almost always multiple ways to wire a bean, multiple ways to structure a package, and multiple valid answers to “where does this logic belong.” The framework is permissive by design — that’s what makes it productive for humans who already know the conventions. It’s also exactly what makes an AI agent underspecified without one.
Three areas cause the most drift from idiomatic Spring Boot:
Dependency injection style. Field injection (@Autowired private UserRepository repo;) still appears constantly in training data because it was the default pattern for years. It works, but it makes classes impossible to unit test without reflection or a running Spring context, and it hides required dependencies. Constructor injection with private final fields is the accepted 2026 standard, and Lombok’s @RequiredArgsConstructor removes the boilerplate cost that used to be the excuse for skipping it.
Layering discipline. Controller → Service → Repository is the textbook layering, but agents without explicit rules will happily put business logic in a controller, call a repository directly from a controller, or put HTTP-specific concerns (status codes, request validation) inside a service class that should be transport-agnostic.
Entity vs. DTO boundaries. Returning a JPA @Entity from a controller is the single most common Spring Boot anti-pattern we see AI agents produce. It works until a lazy-loaded collection triggers a LazyInitializationException outside the transaction, or until the entity’s internal fields — including ones you didn’t mean to expose — get serialized straight into the API response.
Complete CLAUDE.md Template for Java + Spring Boot Projects
This template targets Spring Boot 3.x/4.x on Java 21+ (LTS), and calls out the Java 25 LTS and Spring Boot 4.0 changes where they matter. Adjust the version-specific lines to match your pom.xml or build.gradle.kts.
# Java / Spring Boot Project: [ProjectName]
## Build & Run (Maven)
- Build: `./mvnw clean install`
- Run: `./mvnw spring-boot:run`
- Test: `./mvnw test`
- Test (single class): `./mvnw test -Dtest=UserServiceTest`
- Verify (build + test + checks): `./mvnw verify`
## Build & Run (Gradle — use only if build.gradle.kts exists, not both)
- Build: `./gradlew build`
- Run: `./gradlew bootRun`
- Test: `./gradlew test`
- Verify: `./gradlew check`
## Java & Spring Boot Version
- Check `pom.xml` (`<java.version>`) or `build.gradle.kts` for the actual version
- This project targets Java 21 LTS minimum; do not use preview features unless explicitly enabled
- Spring Boot 4.0 renamed several `spring.` properties — check `application.yml` before assuming Boot 3.x property names
## Dependency Injection
- Constructor injection only. Never use field injection (`@Autowired` on a field)
- Mark injected fields `private final`
- Use Lombok `@RequiredArgsConstructor` to generate the constructor — do not hand-write it
- Never use `@Autowired` on the constructor itself when there is only one constructor (Spring 4.3+ infers it)
## Layering
- Controller: HTTP concerns only — request mapping, status codes, request/response DTOs
- Service: business logic, transaction boundaries (`@Transactional`), orchestration across repositories
- Repository: persistence only — extend `JpaRepository` or `CrudRepository`, no business logic
- Controllers must never call a Repository directly; always go through a Service
## DTOs, Not Entities
- Never return a `@Entity` class from a `@RestController` method
- Define a `record` DTO per use case (e.g. `UserResponse`, `CreateUserRequest`) — do not reuse one DTO for both directions
- Map Entity → DTO in the Service layer, not the Controller
- Use `record` types for DTOs (Java 16+) unless the project's existing DTOs use classes — match existing style
## Exception Handling
- Centralize exception-to-HTTP mapping in a `@RestControllerAdvice` class
- Never let a raw exception reach the client as an unhandled 500
- Define custom exceptions per failure mode (`UserNotFoundException`, `DuplicateEmailException`) rather than throwing generic `RuntimeException`
- Every custom exception maps to exactly one HTTP status in the advice class
## Transactions
- `@Transactional` goes on Service methods, never Repository or Controller methods
- Default to `readOnly = true` on query-only service methods
- Keep transaction scope narrow — do not wrap a whole request lifecycle in one `@Transactional` method that also calls external HTTP clients
## Validation
- Use Bean Validation annotations (`@NotNull`, `@Size`, `@Email`) directly on request DTO fields
- Annotate controller method parameters with `@Valid`
- Do not hand-write null checks in the controller for fields already covered by Bean Validation
## Testing
- Unit tests: JUnit 5 + Mockito, no Spring context — test Services and business logic in isolation
- Integration tests: `@SpringBootTest` with Testcontainers for the real database, not H2, when the project already uses Testcontainers
- Repository tests: `@DataJpaTest` for repository-layer verification
- Naming: `should_returnX_when_Y` or `methodName_condition_expectedResult` — match whichever convention exists in the test directory already
## Lombok
- `@RequiredArgsConstructor` for constructor injection
- `@Getter` on entities and DTOs where needed; avoid `@Data` on JPA entities (it generates `equals`/`hashCode` over all fields, which breaks with lazy associations)
- Never use `@Setter` on entities unless the field is genuinely mutable business state
## Architecture (if using Spring Modulith)
- Module boundaries are defined by top-level packages under the main application package
- Cross-module calls go through a module's public API package only, never into another module's internal packages
- Run `mvn spring-modulith:verify` (or the project's equivalent) before considering a cross-module change complete
Constructor Injection: The Rule That Matters Most
If your CLAUDE.md only enforces one thing, make it this one.
// Bad — field injection
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PaymentClient paymentClient;
}
// Good — constructor injection with Lombok
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
}
Field injection compiles, runs, and passes a manual smoke test. It fails you later: you can’t construct OrderService in a plain JUnit test without a running Spring context or reflection tricks, required dependencies aren’t visible at the type level, and circular dependencies get hidden until runtime instead of failing fast at startup. Constructor injection surfaces circular dependency bugs immediately — Spring refuses to start. That’s a feature, not friction.
The Entity-Leak Anti-Pattern
This is the mistake we see Claude Code (and every other coding agent) reach for by default, because returning the entity directly is the shortest path to a working endpoint.
// Bad — leaks the entity, including lazy associations and internal fields
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// Good — explicit DTO, mapped in the service layer
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
return userService.getUser(id);
}
// In UserService
public UserResponse getUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
return new UserResponse(user.getId(), user.getEmail(), user.getDisplayName());
}
The User entity in the bad example almost certainly has a lazy @OneToMany collection somewhere — orders, addresses, roles. Serialize it directly and one of two things happens: Jackson triggers a LazyInitializationException because the Hibernate session closed before serialization, or the lazy collection loads fine and you’ve just shipped an endpoint that returns an unbounded list nobody asked for. A DTO with exactly the fields the client needs avoids both failure modes and doubles as your API contract.
@RestControllerAdvice for Centralized Exception Handling
Without explicit instruction, an agent handles exceptions inline, inconsistently, in whichever controller method happens to need it that day.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("USER_NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<ErrorResponse> handleDuplicate(DuplicateEmailException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("DUPLICATE_EMAIL", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("VALIDATION_FAILED", ex.getMessage()));
}
}
Tell Claude Code in the CLAUDE.md that every new custom exception must get a corresponding @ExceptionHandler in this class, and it stops writing try/catch blocks scattered across controllers. One place to look, one place to update.
Maven vs. Gradle: Say It Once, Explicitly
Spring’s own samples and the overwhelming majority of AI training data default to Maven. If your project uses Gradle — especially the Kotlin DSL — an unguided agent will periodically generate pom.xml snippets or suggest Maven commands in comments, even in a Gradle-only repo.
The fix is one line, but it has to be explicit:
This project uses Gradle (build.gradle.kts). Do not generate or reference pom.xml, mvnw, or Maven-style dependency declarations.
State the build tool once, at the top of the CLAUDE.md, and the drift stops. Leaving it implicit — because “obviously there’s a build.gradle.kts in the repo” — is exactly the assumption that doesn’t hold once Claude Code is working across a large context window with a mix of file types.
Hook-Driven Verification with Maven or Gradle
Claude Code hooks let you run the build and test suite automatically after every edit, the same pattern that works well for Go and Rust projects, adapted for the JVM’s slower feedback loop.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "./mvnw -q compile"
}
]
}
]
}
}
A full ./mvnw verify on every edit is usually too slow to be useful — JVM startup and Spring context loading alone can take 10-20 seconds. A lighter ./mvnw -q compile (or ./gradlew compileJava for Gradle) catches type errors and missing imports fast, and you reserve the full verify/check run for before a commit, either as a manual step or a Stop hook rather than PostToolUse.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./mvnw test"
}
]
}
]
}
}
Spring Boot 4.0 and Java 25 LTS: What Changed for CLAUDE.md
Spring Boot 4.0 and Java 25 LTS both landed in 2026, and teams migrating onto them run into a specific CLAUDE.md problem: an agent trained on the older ecosystem defaults to Spring Boot 3.x property names and pre-25 Java idioms unless told otherwise.
Two things worth stating explicitly in your template if you’re on the current stack:
- Property renames. Spring Boot 4.0 reorganized several
spring.*configuration properties. If Claude Code suggests a property key from a Boot 3.x tutorial it half-remembers, point it at the actualapplication.ymlin the repo rather than trusting the property name from training data. - Spring Modulith for module boundaries. Teams adopting Spring Modulith alongside Spring Boot 4.0 are using it to enforce hexagonal-style boundaries (
domain→application→adapter.in/adapter.out) inside a single deployable. If your project uses this pattern, add the module-boundary rule from the template above — otherwise Claude Code will happily import across module internals because nothing in a plain Java compiler stops it.
AGENTS.md Compatible Version
If you’re using a tool that reads AGENTS.md instead of or alongside CLAUDE.md, here’s a condensed version:
# AGENTS.md — Java / Spring Boot Project
## Commands
- build: `./mvnw clean install` (or `./gradlew build`)
- test: `./mvnw test` (or `./gradlew test`)
- run: `./mvnw spring-boot:run` (or `./gradlew bootRun`)
## Critical Rules
1. Constructor injection only; never field injection
2. Controller → Service → Repository; controllers never call repositories directly
3. Never return a JPA entity from a controller; always map to a DTO in the service layer
4. Centralize exception handling in one @RestControllerAdvice class
5. @Transactional on service methods only, readOnly = true for query-only methods
6. Bean Validation annotations on DTOs, @Valid on controller parameters
7. Use the project's actual build tool (Maven or Gradle) — never mix pom.xml and build.gradle.kts
## Testing
- Unit tests: JUnit 5 + Mockito, no Spring context
- Repository tests: @DataJpaTest
- Integration tests: @SpringBootTest + Testcontainers
Common AI + Spring Boot Mistakes to Watch For
Even with a CLAUDE.md in place, these patterns show up in code review often enough to call out explicitly:
@Data on JPA entities. Lombok’s @Data generates equals() and hashCode() over every field, including lazy-loaded associations. Calling equals() on an entity with an unloaded collection triggers a LazyInitializationException or, worse, a full unintended fetch. Use @Getter and explicit equals()/hashCode() based on the ID field only.
Catching and swallowing exceptions in the service layer. An agent that adds error handling defensively will sometimes wrap a repository call in try/catch and return null or an empty Optional on failure, silently hiding a database error that should have propagated to the @RestControllerAdvice.
@Transactional on a method that calls an external HTTP client. This holds a database connection open for the duration of the external call, which is a common source of connection pool exhaustion under load. Transaction scope should cover database work only.
Generating a new DTO record for every method instead of reusing one per concept. Left unguided, agents will create UserDto, UserResponseDto, and UserResponse in the same PR for the same shape. State the naming convention once in CLAUDE.md and point to an existing DTO as the pattern to follow.
Skipping @Valid on nested objects. Bean Validation doesn’t cascade into nested DTOs automatically — a nested object field needs its own @Valid annotation, or its validation constraints are silently ignored.
The pattern here mirrors what works for any strongly-conventioned framework: state the layering explicitly, name the anti-patterns you don’t want, and wire a fast compile check into a hook so Claude Code catches its own mistakes before you do.
Browse a real Spring Boot + JPA rules file in our gallery for a starting point you can adapt.
FAQ
Does Claude Code know Spring Boot 4.0’s property renames automatically?
Not reliably. Training data includes years of Spring Boot 3.x tutorials, so an agent may suggest an old property name from memory. Point it at the actual application.yml in the repo, and note the Boot 4.0 change explicitly in CLAUDE.md if your project has already migrated.
Should CLAUDE.md go in the root or in each module for a multi-module Maven project?
Root is enough for most projects, since the build tool, DI style, and layering rules apply project-wide. For a multi-module monorepo with genuinely different conventions per module (e.g. a legacy module vs. a new Spring Modulith module), a CLAUDE.md per module directory that only overrides what differs is cleaner than duplicating the whole file.
Is Lombok still recommended in 2026 given Java records and modern language features?
Yes for reducing constructor/getter boilerplate on service and DI classes, but avoid @Data on entities specifically. For DTOs, prefer Java record types over Lombok where the DTO is truly immutable — records give you equals/hashCode/toString for free without Lombok’s entity pitfalls.
Can this template work with Cursor or GitHub Copilot instead of Claude Code?
The AGENTS.md version above is tool-agnostic and works with any agent that reads that file. The full CLAUDE.md template’s content is not Claude-specific — Cursor and other tools reading it (renamed to their own convention file) will follow the same rules.