spring-boot-patterns — Claude Code skill: Spring Boot layering
A skill that hands your agent the package layout and templates for Spring Boot controllers, services and repositories. Useful when new code gets written or reviewed.
npx skills add decebals/claude-code-java --skill spring-boot-patterns -a claude-codeWhat spring-boot-patterns does
The skill fixes a package layout: controller, service with an impl subpackage, repository, model, dto split into request and response, plus exception, config and util. Then come the templates — a REST controller on a versioned path like /api/v1/users, a service interface with its implementation, a JPA repository with derived queries and @Query, record DTOs validated by @NotBlank, @Email and @Min, and a MapStruct mapper.
There is a separate anti-patterns section. Business logic inside a controller, hitting the repository straight from the controller, returning an entity instead of a DTO: all flagged as wrong. The skill also pushes @Transactional(readOnly = true) at class level and @Transactional on write methods.
JPA and Hibernate behaviour is out of scope — the description points you to the jpa-patterns skill for that. Templates assume Spring Boot; a plain Spring project or another framework gets little from them. The examples use Lombok (@RequiredArgsConstructor) and MapStruct, so those need to be on the classpath.
What it helps with
- lay out packages for a new Spring Boot project
- write a REST controller with a versioned path and proper status codes
- move business logic out of a controller into a service
- swap an exposed entity for a DTO with a mapper
- review existing code for layer violations
Example prompts for the agent
- create an orders controller following spring-boot-patterns
- add a service and repository for the User entity like in spring-boot-patterns
- check this class against the anti-patterns in spring-boot-patterns
How to install spring-boot-patterns
1Skills CLI (Vercel)
npx skills add decebals/claude-code-java --skill spring-boot-patterns -a claude-code2Manually: put the files in place
~/.claude/skills/spring-boot-patterns/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/spring-boot-patterns/
1Skills CLI (Vercel)
npx skills add decebals/claude-code-java --skill spring-boot-patterns -a codex2Manually: put the files in place
.agents/skills/spring-boot-patterns/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add decebals/claude-code-java --skill spring-boot-patterns -a cursor2Manually: put the files in place
.cursor/skills/spring-boot-patterns/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install decebals/claude-code-java spring-boot-patterns2Manually: put the files in place
.github/skills/spring-boot-patterns/SKILL.md1Skills CLI (Vercel)
npx skills add decebals/claude-code-java --skill spring-boot-patterns -a gemini-cli2Platform CLI
gemini skills install https://github.com/decebals/claude-code-java --path skills/spring-boot-patternsManually: put the files in place
.devin/skills/spring-boot-patterns/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add decebals/claude-code-java --skill spring-boot-patterns -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about spring-boot-patterns
How do I install spring-boot-patterns in Claude?
Run npx skills add decebals/claude-code-java --skill spring-boot-patterns -a claude-code. The skill file lands at ~/.claude/skills/spring-boot-patterns/SKILL.md.
What does spring-boot-patterns do?
It gives the agent Spring Boot templates and rules: package structure, controllers, services, repositories, DTOs and exception handling, plus a list of anti-patterns to avoid.
How is spring-boot-patterns different from jpa-patterns?
This one covers application layering and wiring; the other covers JPA and Hibernate behaviour. For entity and query questions the description sends you to jpa-patterns.
Source text
SKILL.md · MIT
---
name: spring-boot-patterns
description: Spring Boot best practices and patterns. Use when creating controllers, services or repositories, or when the user asks about Spring Boot layering, wiring, configuration or exception handling. For JPA and Hibernate behaviour, use jpa-patterns instead.
license: MIT
---
# 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
```java
@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 | `/api/v1/users` |
| Plural nouns | `/users` not `/user` |
| HTTP methods | GET=read, POST=create, PUT=update, DELETE=delete |
| Status codes | 200=OK, 201=Created, 204=NoContent, 404=NotFound |
| Validation | `@Valid` on request body |
### ❌ Anti-patterns
```java
// ❌ 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
```java
// 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
- `@Transactional(readOnly = true)` at class level
- `@Transactional` for write methods
- Throw domain exceptions, not generic ones
- Use mappers (MapStruct) for entity ↔ DTO conversion
---
## Repository Patterns
### JPA Repository
```java
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
- `Optional` for single results
- `existsBy` instead of `findBy` for existence checks
- Avoid native queries unless necessary
- Use `@EntityGraph` for fetch optimization
---
## DTO Patterns
### Request/Response DTOs
```java
// 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
```java
@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
```java
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
```java
@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
```yaml
# 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
```java
@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 |
|------------|---------|
| `@RestController` | REST controller (combines @Controller + @ResponseBody) |
| `@Service` | Business logic component |
| `@Repository` | Data access component |
| `@Configuration` | Configuration class |
| `@RequiredArgsConstructor` | Lombok: constructor injection |
| `@Transactional` | Transaction management |
| `@Valid` | Trigger validation |
| `@ConfigurationProperties` | Bind properties to class |
| `@Profile("dev")` | Profile-specific bean |
| `@Scheduled` | Scheduled tasks |
---
## Testing Patterns
### Controller Test (MockMvc)
```java
@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", "[email protected]", null));
mockMvc.perform(get("/api/v1/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"));
}
}
```
### Service Test
```java
@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
```java
@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": "[email protected]", "age": 25}
"""))
.andExpect(status().isCreated());
}
}
```
---
## Quick Reference Card
| Layer | Responsibility | Annotations |
|-------|---------------|-------------|
| Controller | HTTP handling, validation | `@RestController`, `@Valid` |
| Service | Business logic, transactions | `@Service`, `@Transactional` |
| Repository | Data access | `@Repository`, extends `JpaRepository` |
| DTO | Data transfer | Records with validation annotations |
| Config | Configuration | `@Configuration`, `@ConfigurationProperties` |
| Exception | Error handling | `@RestControllerAdvice` |
© decebals · MIT · decebals/claude-code-java