Claude-code-java spring-boot-patterns
Spring Boot best practices and patterns. Use when creating controllers, services, repositories, or when user asks about Spring Boot architecture, REST APIs, exception handling, or JPA patterns.
install
source · Clone the upstream repo
git clone https://github.com/decebals/claude-code-java
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/decebals/claude-code-java "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/spring-boot-patterns" ~/.claude/skills/decebals-claude-code-java-spring-boot-patterns && rm -rf "$T"
manifest:
.claude/skills/spring-boot-patterns/SKILL.mdsource content
Spring Boot Patterns Skill
Best practices and patterns for Spring Boot applications.
When to Use
- User says "create controller" / "add service" / "Spring Boot help"
- Reviewing Spring Boot code
- Setting up new Spring Boot project structure
Project Structure
src/main/java/com/example/myapp/ ├── MyAppApplication.java # @SpringBootApplication ├── config/ # Configuration classes │ ├── SecurityConfig.java │ └── WebConfig.java ├── controller/ # REST controllers │ └── UserController.java ├── service/ # Business logic │ ├── UserService.java │ └── impl/ │ └── UserServiceImpl.java ├── repository/ # Data access │ └── UserRepository.java ├── model/ # Entities │ └── User.java ├── dto/ # Data transfer objects │ ├── request/ │ │ └── CreateUserRequest.java │ └── response/ │ └── UserResponse.java ├── exception/ # Custom exceptions │ ├── ResourceNotFoundException.java │ └── GlobalExceptionHandler.java └── util/ # Utilities └── DateUtils.java
Controller Patterns
REST Controller Template
@RestController @RequestMapping("/api/v1/users") @RequiredArgsConstructor // Lombok for constructor injection public class UserController { private final UserService userService; @GetMapping public ResponseEntity<List<UserResponse>> getAll() { return ResponseEntity.ok(userService.findAll()); } @GetMapping("/{id}") public ResponseEntity<UserResponse> getById(@PathVariable Long id) { return ResponseEntity.ok(userService.findById(id)); } @PostMapping public ResponseEntity<UserResponse> create( @Valid @RequestBody CreateUserRequest request) { UserResponse created = userService.create(request); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path("/{id}") .buildAndExpand(created.getId()) .toUri(); return ResponseEntity.created(location).body(created); } @PutMapping("/{id}") public ResponseEntity<UserResponse> update( @PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { return ResponseEntity.ok(userService.update(id, request)); } @DeleteMapping("/{id}") public ResponseEntity<Void> delete(@PathVariable Long id) { userService.delete(id); return ResponseEntity.noContent().build(); } }
Controller Best Practices
| Practice | Example |
|---|---|
| Versioned API | |
| Plural nouns | not |
| HTTP methods | GET=read, POST=create, PUT=update, DELETE=delete |
| Status codes | 200=OK, 201=Created, 204=NoContent, 404=NotFound |
| Validation | on request body |
❌ Anti-patterns
// ❌ Business logic in controller @PostMapping public User create(@RequestBody User user) { user.setCreatedAt(LocalDateTime.now()); // Logic belongs in service return userRepository.save(user); // Direct repo access } // ❌ Returning entity directly (exposes internals) @GetMapping("/{id}") public User getById(@PathVariable Long id) { return userRepository.findById(id).get(); }
Service Patterns
Service Interface + Implementation
// Interface public interface UserService { List<UserResponse> findAll(); UserResponse findById(Long id); UserResponse create(CreateUserRequest request); UserResponse update(Long id, UpdateUserRequest request); void delete(Long id); } // Implementation @Service @RequiredArgsConstructor @Transactional(readOnly = true) // Default read-only public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMapper userMapper; @Override public List<UserResponse> findAll() { return userRepository.findAll().stream() .map(userMapper::toResponse) .toList(); } @Override public UserResponse findById(Long id) { return userRepository.findById(id) .map(userMapper::toResponse) .orElseThrow(() -> new ResourceNotFoundException("User", id)); } @Override @Transactional // Write transaction public UserResponse create(CreateUserRequest request) { User user = userMapper.toEntity(request); User saved = userRepository.save(user); return userMapper.toResponse(saved); } @Override @Transactional public void delete(Long id) { if (!userRepository.existsById(id)) { throw new ResourceNotFoundException("User", id); } userRepository.deleteById(id); } }
Service Best Practices
- Interface + Impl for testability
at class level@Transactional(readOnly = true)
for write methods@Transactional- Throw domain exceptions, not generic ones
- Use mappers (MapStruct) for entity ↔ DTO conversion
Repository Patterns
JPA Repository
public interface UserRepository extends JpaRepository<User, Long> { // Derived query Optional<User> findByEmail(String email); List<User> findByActiveTrue(); // Custom query @Query("SELECT u FROM User u WHERE u.department.id = :deptId") List<User> findByDepartmentId(@Param("deptId") Long departmentId); // Native query (use sparingly) @Query(value = "SELECT * FROM users WHERE created_at > :date", nativeQuery = true) List<User> findRecentUsers(@Param("date") LocalDate date); // Exists check (more efficient than findBy) boolean existsByEmail(String email); // Count long countByActiveTrue(); }
Repository Best Practices
- Use derived queries when possible
for single resultsOptional
instead ofexistsBy
for existence checksfindBy- Avoid native queries unless necessary
- Use
for fetch optimization@EntityGraph
DTO Patterns
Request/Response DTOs
// Request DTO with validation public record CreateUserRequest( @NotBlank(message = "Name is required") @Size(min = 2, max = 100) String name, @NotBlank @Email(message = "Invalid email format") String email, @NotNull @Min(18) Integer age ) {} // Response DTO public record UserResponse( Long id, String name, String email, LocalDateTime createdAt ) {}
MapStruct Mapper
@Mapper(componentModel = "spring") public interface UserMapper { UserResponse toResponse(User entity); List<UserResponse> toResponseList(List<User> entities); @Mapping(target = "id", ignore = true) @Mapping(target = "createdAt", ignore = true) User toEntity(CreateUserRequest request); }
Exception Handling
Custom Exceptions
public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String resource, Long id) { super(String.format("%s not found with id: %d", resource, id)); } } public class BusinessException extends RuntimeException { private final String code; public BusinessException(String code, String message) { super(message); this.code = code; } }
Global Exception Handler
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) { log.warn("Resource not found: {}", ex.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ErrorResponse("NOT_FOUND", ex.getMessage())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidation( MethodArgumentNotValidException ex) { List<String> errors = ex.getBindingResult().getFieldErrors().stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()) .toList(); return ResponseEntity.badRequest() .body(new ErrorResponse("VALIDATION_ERROR", errors.toString())); } @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) { log.error("Unexpected error", ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred")); } } public record ErrorResponse(String code, String message) {}
Configuration Patterns
Application Properties
# application.yml spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: ${DB_USER} password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate # Never 'create' in production! show-sql: false app: jwt: secret: ${JWT_SECRET} expiration: 86400000
Configuration Properties Class
@Configuration @ConfigurationProperties(prefix = "app.jwt") @Validated public class JwtProperties { @NotBlank private String secret; @Min(60000) private long expiration; // getters and setters }
Profile-Specific Configuration
src/main/resources/ ├── application.yml # Common config ├── application-dev.yml # Development ├── application-test.yml # Testing └── application-prod.yml # Production
Common Annotations Quick Reference
| Annotation | Purpose |
|---|---|
| REST controller (combines @Controller + @ResponseBody) |
| Business logic component |
| Data access component |
| Configuration class |
| Lombok: constructor injection |
| Transaction management |
| Trigger validation |
| Bind properties to class |
| Profile-specific bean |
| Scheduled tasks |
Testing Patterns
Controller Test (MockMvc)
@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void shouldReturnUser() throws Exception { when(userService.findById(1L)) .thenReturn(new UserResponse(1L, "John", "john@example.com", null)); mockMvc.perform(get("/api/v1/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("John")); } }
Service Test
@ExtendWith(MockitoExtension.class) class UserServiceImplTest { @Mock private UserRepository userRepository; @Mock private UserMapper userMapper; @InjectMocks private UserServiceImpl userService; @Test void shouldThrowWhenUserNotFound() { when(userRepository.findById(1L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> userService.findById(1L)) .isInstanceOf(ResourceNotFoundException.class); } }
Integration Test
@SpringBootTest @AutoConfigureMockMvc @Testcontainers class UserIntegrationTest { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15"); @Autowired private MockMvc mockMvc; @Test void shouldCreateUser() throws Exception { mockMvc.perform(post("/api/v1/users") .contentType(MediaType.APPLICATION_JSON) .content(""" {"name": "John", "email": "john@example.com", "age": 25} """)) .andExpect(status().isCreated()); } }
Quick Reference Card
| Layer | Responsibility | Annotations |
|---|---|---|
| Controller | HTTP handling, validation | , |
| Service | Business logic, transactions | , |
| Repository | Data access | , extends |
| DTO | Data transfer | Records with validation annotations |
| Config | Configuration | , |
| Exception | Error handling | |