Claude Code with Spring Boot: Java Developer Workflow
Why Spring Boot developers need deliberate Claude Code configuration
Claude Code understands Spring Boot. It knows @RestController, @Service, @Repository, Spring Data JPA, Spring Security, and the standard layered architecture. The gap between "Claude knows Spring" and "Claude writes Spring Boot code that fits your project" is the same gap it is for every framework: general knowledge without project-specific context.
Spring Boot projects accumulate conventions over years. How your DTOs are structured, whether you use MapStruct or manual mapping, which validation annotations you use and where, how exceptions propagate through the layers, and whether you use Spring Modulith or a flat package structure. Claude will guess at all of these without a CLAUDE.md and generate code that compiles but does not fit.
Java's verbosity also means convention drift is expensive to fix. A single generated @RestController with the wrong exception handling strategy, missing validation annotations, or incorrect DTO mapping pattern requires significant rework. Getting the CLAUDE.md right once eliminates that category of fix permanently.
This guide covers the CLAUDE.md configuration for Spring Boot projects, @RestController and @Service patterns, JPA entity conventions, Mockito and @WebMvcTest test setup, and permission hooks for Flyway and Maven. If you have not set up Claude Code yet, the Claude Code setup guide covers installation and authentication before project configuration applies.
The Spring Boot CLAUDE.md
The CLAUDE.md at your project root is read before every Claude Code session. For a Spring Boot project, it needs to answer: what Java and Spring Boot versions are in use, how is the project structured, which Spring modules are active, how are exceptions handled, how is validation done, and how are tests run?
# Spring Boot project rules
## Versions
- Java: 21 (use record classes, pattern matching, text blocks where appropriate)
- Spring Boot: 3.3
- Build tool: Maven (./mvnw for all commands, never bare mvn)
- Lombok: yes, use @Data, @Builder, @RequiredArgsConstructor, @Slf4j
## Project structure
- src/main/java/com/example/service/
- api/ , REST controllers, request/response DTOs
- application/ , application services (use cases)
- domain/ , domain models, repository interfaces, domain services
- infrastructure/, JPA entities, repository implementations, external clients
- config/ , Spring configuration classes
## Active Spring modules
- Spring Data JPA (primary ORM)
- Spring Security (JWT-based, stateless)
- Spring Validation (jakarta.validation)
- Spring Actuator (health, info, metrics)
- No Spring MVC views, JSON API only
## Exception handling
- Global exception handler: GlobalExceptionHandler.java (@RestControllerAdvice)
- All exceptions caught and mapped in GlobalExceptionHandler, never return raw exceptions
- Business exceptions extend BusinessException (unchecked)
- Validation errors return 400 with field-level error details
- Never throw HttpStatusCodeException from service layer
## Validation
- Request validation: @Valid on @RequestBody parameters, annotations on DTO fields
- Use jakarta.validation only (not javax.validation)
- Custom validators in infrastructure/validation/ package
## Hard rules
- Controllers never contain business logic, delegate to application services
- Application services never import JPA entities directly
- Domain models never import Spring annotations
- No @Transactional on controllers
- No field injection (@Autowired on fields), use constructor injection only
- Records for immutable DTOs, classes for mutable domain objects
Three sections in this CLAUDE.md prevent the most common Claude Code failures with Spring Boot.
The project structure section (using a hexagonal or layered structure) is the highest-leverage configuration. Without it, Claude places controllers, services, and repositories in a flat com.example package. For anything beyond a small project, this makes the codebase unnavigable and breaks Claude's own ability to find related files across sessions. The separation of api/, application/, domain/, and infrastructure/ also makes the architecture intent explicit, which Claude uses when generating new classes.
The field injection rule matters specifically for Spring Boot. Claude's training data includes a significant amount of Spring code from older codebases that uses @Autowired field injection. Constructor injection is the current Spring best practice for two reasons: it makes dependencies explicit and makes classes testable without a Spring context. Without the rule, Claude will generate @Autowired fields in service classes, which breaks @WebMvcTest-style unit tests that do not spin up the full context.
The layer isolation rules prevent leaks across boundaries. The most common violation Claude generates without guidance is application services that import JPA entity classes directly, bypassing the domain layer. The explicit rule "application services never import JPA entities directly" keeps the mapping layer in infrastructure/ where it belongs.
RestController patterns
Spring Boot's @RestController layer is where Claude Code produces consistent output when given clear patterns. The critical thing to establish is how the controller delegates to the service layer and how response DTOs are structured.
Add to CLAUDE.md:
## RestController conventions
### Controller structure
@RestController
@RequestMapping("/api/v1/items")
@RequiredArgsConstructor
@Tag(name = "Items", description = "Item management")
public class ItemController {
private final ItemApplicationService itemService;
@GetMapping
public ResponseEntity<PageResponse<ItemResponse>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(itemService.findAll(page, size));
}
@PostMapping
public ResponseEntity<ItemResponse> create(
@Valid @RequestBody CreateItemRequest request) {
ItemResponse created = itemService.create(request);
URI location = URI.create("/api/v1/items/" + created.id());
return ResponseEntity.created(location).body(created);
}
@GetMapping("/{id}")
public ResponseEntity<ItemResponse> get(@PathVariable Long id) {
return ResponseEntity.ok(itemService.findById(id));
}
@PutMapping("/{id}")
public ResponseEntity<ItemResponse> update(
@PathVariable Long id,
@Valid @RequestBody UpdateItemRequest request) {
return ResponseEntity.ok(itemService.update(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
itemService.delete(id);
return ResponseEntity.noContent().build();
}
}
### DTO conventions (use Java records for response DTOs)
public record ItemResponse(
Long id,
String name,
String description,
BigDecimal price,
Instant createdAt
) {}
### Request DTO (class for mutable objects with validation)
@Data
@Builder
public class CreateItemRequest {
@NotBlank(message = "Name is required")
@Size(max = 255)
private String name;
private String description;
@NotNull(message = "Price is required")
@DecimalMin(value = "0.01", message = "Price must be positive")
private BigDecimal price;
}
The ResponseEntity.created(location).body(created) pattern for POST responses is worth making explicit. Claude will default to ResponseEntity.ok() for all success responses without guidance, missing the 201 Created status and Location header that REST conventions require for resource creation.
Java records for response DTOs is a Java 21 convention that Claude supports but will not use by default for Spring Boot classes. Records are immutable, have built-in equals, hashCode, and toString, and require no Lombok annotations. For response objects that are only written once and read many times, records are strictly better than @Data classes. The explicit convention in CLAUDE.md makes Claude use records for response types consistently.
Service layer with @Service
The application service layer is where business logic lives. Claude Code generates clean service classes when the pattern is explicit, but will mix layer concerns without guidance.
Add to CLAUDE.md:
## Application service conventions
### Service structure
@Service
@RequiredArgsConstructor
@Slf4j
public class ItemApplicationService {
private final ItemRepository itemRepository;
private final ItemMapper mapper;
@Transactional(readOnly = true)
public PageResponse<ItemResponse> findAll(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<ItemEntity> result = itemRepository.findAll(pageable);
return PageResponse.of(result.map(mapper::toResponse));
}
@Transactional
public ItemResponse create(CreateItemRequest request) {
log.debug("Creating item: {}", request.getName());
ItemEntity entity = mapper.toEntity(request);
ItemEntity saved = itemRepository.save(entity);
return mapper.toResponse(saved);
}
@Transactional(readOnly = true)
public ItemResponse findById(Long id) {
return itemRepository.findById(id)
.map(mapper::toResponse)
.orElseThrow(() -> new ItemNotFoundException(id));
}
}
### Transaction rules
- @Transactional(readOnly = true) on all read methods
- @Transactional on all write methods
- Never @Transactional on controller methods
- Never call @Transactional methods from within the same class (Spring proxy limitation)
### Business exceptions
public class ItemNotFoundException extends BusinessException {
public ItemNotFoundException(Long id) {
super("Item not found: " + id, HttpStatus.NOT_FOUND);
}
}
The @Transactional(readOnly = true) pattern for read methods is worth making explicit. JPA providers like Hibernate use this hint to skip dirty checking, which reduces memory usage and improves performance for read-heavy operations. Claude knows this optimization but will omit it without an explicit rule.
The "never call @Transactional methods from within the same class" rule prevents a Spring proxy gotcha. Spring's @Transactional works via proxy objects. When a method on the same class calls another @Transactional method directly (not via the proxy), the transaction is not applied. This catches developers off guard and is a pattern Claude will generate when writing chained service calls.
JPA entity conventions
JPA entities are the infrastructure layer representation of your domain objects. Claude Code needs explicit conventions here because there is significant variation in how the community handles entity mapping, audit fields, and relationship loading.
Add to CLAUDE.md:
## JPA entity conventions
### Entity structure
@Entity
@Table(name = "items")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public class ItemEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 255)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@CreatedDate
@Column(nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(nullable = false)
private Instant updatedAt;
}
### Relationship loading rules
- Default fetch type: LAZY for all relationships (override in queries, not on the field)
- Use @EntityGraph on repository methods for eager loading where needed
- Never use FetchType.EAGER on @OneToMany or @ManyToMany
- N+1 prevention: use JOIN FETCH in JPQL or @EntityGraph, never access lazy collections outside a transaction
### Naming conventions
- Entity classes: {Name}Entity to distinguish from domain models
- Table names: snake_case plural (items, order_lines, product_categories)
- Column names: snake_case, always explicit, never rely on Hibernate naming strategy
The FetchType.LAZY rule for all relationships is critical for performance. Claude will sometimes generate FetchType.EAGER on @ManyToOne relationships as a convenience. For large entity graphs, EAGER loading causes Hibernate to JOIN every associated table on every query, turning a simple lookup into a multi-table join that loads far more data than needed.
The {Name}Entity naming convention prevents a subtle confusion Claude runs into without guidance: when domain model classes and JPA entity classes have the same name in different packages, Claude sometimes generates imports from the wrong package. The Entity suffix makes the import unambiguous.
Mockito and WebMvcTest patterns
Testing Spring Boot applications effectively requires different test setups depending on the layer. Claude Code produces clean tests when the correct setup is in CLAUDE.md.
Add to CLAUDE.md:
## Testing conventions
### Controller tests (@WebMvcTest, no full context)
@WebMvcTest(ItemController.class)
@AutoConfigureMockMvc(addFilters = false) // disable security for unit tests
class ItemControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ItemApplicationService itemService;
@Autowired
private ObjectMapper objectMapper;
@Test
void createItem_returnsCreated() throws Exception {
CreateItemRequest request = CreateItemRequest.builder()
.name("Widget")
.price(new BigDecimal("9.99"))
.build();
ItemResponse response = new ItemResponse(1L, "Widget", null,
new BigDecimal("9.99"), Instant.now());
given(itemService.create(any(CreateItemRequest.class))).willReturn(response);
mockMvc.perform(post("/api/v1/items")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.name").value("Widget"));
}
}
### Service tests (pure unit tests with Mockito)
@ExtendWith(MockitoExtension.class)
class ItemApplicationServiceTest {
@Mock
private ItemRepository itemRepository;
@Mock
private ItemMapper mapper;
@InjectMocks
private ItemApplicationService itemService;
@Test
void findById_throwsNotFoundException_whenItemMissing() {
given(itemRepository.findById(99L)).willReturn(Optional.empty());
assertThatThrownBy(() -> itemService.findById(99L))
.isInstanceOf(ItemNotFoundException.class)
.hasMessageContaining("99");
}
}
### Integration tests (@SpringBootTest)
// Use @SpringBootTest only for full integration tests
// Use Testcontainers for database: @Testcontainers, @Container for PostgreSQL
// Test profile: application-test.yml, separate DB, no external API calls
- Run: ./mvnw test
- Run specific: ./mvnw test -Dtest=ItemControllerTest
- Skip integration: ./mvnw test -DskipITs
The @AutoConfigureMockMvc(addFilters = false) annotation is essential for controller unit tests in secured applications. Without it, @WebMvcTest applies Spring Security filters, which causes all requests to return 401 Unauthorized unless you configure the full security context. For controller logic tests, disabling security filters and testing authentication separately is cleaner.
The BDDMockito.given() style (given(mock.method()).willReturn(value)) reads more naturally than Mockito.when() style in test scenarios. Claude knows both but defaults to when().thenReturn() without guidance. The given/willReturn pair maps directly to Given/When/Then test structure, making test intent clearer.
The Claude Code testing guide covers broader test generation workflows including how to use Claude Code for TDD where the test is written before the implementation, a pattern that works particularly well with Spring Boot's layered architecture.
Permission hooks for Spring Boot projects
Spring Boot projects have several commands that need gating. Flyway migrations, Maven lifecycle phases that deploy artifacts, and any command that modifies the database schema are the primary candidates.
In .claude/settings.local.json:
{
"permissions": {
"allow": [
"Bash(./mvnw compile*)",
"Bash(./mvnw test*)",
"Bash(./mvnw verify*)",
"Bash(./mvnw spring-boot:run*)",
"Bash(./mvnw dependency:tree*)",
"Bash(./mvnw help:effective-pom*)"
],
"deny": [
"Bash(./mvnw deploy*)",
"Bash(./mvnw release*)",
"Bash(flyway migrate*)",
"Bash(flyway repair*)",
"Bash(flyway clean*)",
"Bash(./mvnw flyway:migrate*)",
"Bash(./mvnw flyway:clean*)"
]
}
}
This setup lets Claude compile, test, verify, and run the application locally without restriction. It blocks deploying Maven artifacts to a repository, running the Maven Release Plugin (which commits version changes and pushes tags), and applying or cleaning Flyway migrations.
The Flyway clean command is particularly important to block. flyway clean drops all tables and recreates the schema from scratch. Claude will not run this intentionally, but it is the kind of command that appears in onboarding scripts and could be invoked as part of a "reset to clean state" request. Blocking it explicitly prevents that category of accident.
For the full coverage of how Claude Code permission hooks work and what other commands are worth gating across project types, the Claude Code hooks guide has the complete permission system reference.
MapStruct conventions
MapStruct is the standard bean mapping library for Spring Boot projects. Claude Code will generate manual mapping code or use ModelMapper without guidance, both of which are harder to maintain.
Add to CLAUDE.md:
## MapStruct conventions
### Mapper interface
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface ItemMapper {
ItemResponse toResponse(ItemEntity entity);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
ItemEntity toEntity(CreateItemRequest request);
List<ItemResponse> toResponseList(List<ItemEntity> entities);
}
### Rules
- componentModel = "spring" always (enables @Autowired injection)
- unmappedTargetPolicy = ReportingPolicy.ERROR (fail at compile time on unmapped fields)
- Ignore generated fields (id, createdAt, updatedAt) explicitly on create mappers
- Never use @Mapping(target = "field", expression = "java(...)"), use a @Named method instead
The unmappedTargetPolicy = ReportingPolicy.ERROR setting turns unmapped target fields into compile errors. Without it, MapStruct silently leaves fields as null when the source and target field names do not match, which causes runtime bugs that are hard to trace. Making it a compile error means Claude's generated mappers are complete by construction.
What Spring Boot developers get wrong first
Three patterns come up consistently when Java developers start using Claude Code on production Spring Boot projects.
Not specifying Spring Boot version. Spring Boot 3.x uses jakarta.validation instead of javax.validation, requires Java 17 minimum, and has different auto-configuration for Spring Security. Claude will generate javax imports, Spring Security 5 config, and pre-3.x patterns if the version is not explicit in CLAUDE.md. One line in the versions section fixes all three.
Missing the @Transactional scope rule. Claude will sometimes place @Transactional on controller methods or on private service methods. Neither does anything: controller transactions span too wide a scope (including HTTP parsing overhead) and private methods bypass the Spring proxy. The explicit rule in CLAUDE.md prevents both patterns.
No DTO/entity boundary in generated code. Claude will generate controller methods that return JPA entity objects directly instead of mapping through DTOs. This exposes the database schema through the API, makes versioning impossible, and causes LazyInitializationException when Jackson tries to serialize lazy-loaded relationships. The rule "controllers never return entity objects, always DTOs" with a mapper example in CLAUDE.md closes this gap.
Getting more from your Spring Boot workflow
The CLAUDE.md configuration in this guide produces a Spring Boot setup where layer boundaries are enforced, JPA entities have consistent conventions, Mockito tests are generated with the right slice annotations, and Flyway migrations cannot be applied without explicit permission.
Spring Boot is one of the highest-leverage targets for Claude Code configuration because the framework is opinionated and the conventions are well-established. Once your CLAUDE.md captures your project's specific variant of those conventions, Claude Code generates service classes, controllers, and tests that fit without correction.
The Claude Code best practices guide covers the configuration principles that apply across languages. For Java and Spring Boot specifically, the investment in a thorough CLAUDE.md pays off faster than in most frameworks because the alternative, correcting convention drift across verbose Java classes, is expensive per occurrence.
For teams running multiple Spring Boot microservices, the Claude Code custom agents guide covers how to set up service-specific subagents with their own CLAUDE.md files, so Claude Code can work across your service mesh without mixing conventions between services.
Claudify includes a Spring Boot CLAUDE.md template as part of the Claude Code workflow kit, pre-configured for Spring Boot 3.3, Flyway permission hooks, MapStruct conventions, and Mockito test patterns.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify