spring-boot-patterns
Skill763 starsChanged 56 days agoMITView on GitHub
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.
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 184
Use mappers (MapStruct) for entity ↔ DTO conversion
- Distinctive: line 218
Use derived queries when possible
- Distinctive: line 221
Avoid native queries unless necessary
- Very specific: line 222
Use @EntityGraph for fetch optimization
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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.javaController 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 | /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
// ❌ 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
@Transactional(readOnly = true)at class level@Transactionalfor write methods- 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
Optionalfor single resultsexistsByinstead offindByfor existence checks- Avoid native queries unless necessary
- Use
@EntityGraphfor fetch optimization
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: 86400000Configuration 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 # ProductionCommon 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)
@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 | @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 |
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 1
- mvonline/claude-skills★ 098% same
Wrote their own version · 1
- zapek/Xeres★ 222same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository17 other files it gives its agents.
- api-contract-reviewSkill
- architecture-reviewSkill
- changelog-generatorSkill
- clean-codeSkill
- concurrency-reviewSkill
- design-patternsSkill
- git-commitSkill
- issue-triageSkill
- java-code-reviewSkill
- java-migrationSkill
- jpa-patternsSkill
- logging-patternsSkill
- maven-dependency-auditSkill
- performance-smell-detectionSkill
- security-auditSkill
- solid-principlesSkill
- test-qualitySkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

