| name | springboot |
| description | [Applies to: **/*.java] Enforces modern Spring Boot best practices for Java applications, covering code structure, dependency injection, API design, error handling, and testing to ensure maintainable, performant, and secure microservices. |
| source | cursor_mdc |
springboot Best Practices
This guide outlines the definitive best practices for developing Spring Boot 3.x applications with Java 17+. Adhere to these rules to build robust, maintainable, and performant services.
1. Code Organization and Structure
Organize your codebase by feature or bounded context, not by technical layer. This improves navigability, cohesion, and testability.
❌ BAD: Technical Layering
com.myapp.project.controller.UserController
com.myapp.project.service.UserService
com.myapp.project.repository.UserRepository
✅ GOOD: Feature-based (Bounded Context)
package com.myapp.project;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ProjectApplication {
public static void main(String[] args) {
SpringApplication.run(ProjectApplication.class, args);
}
}
package com.myapp.project.user;
package com.myapp.project.user.api;
package com.myapp.project.user.domain;
package com.myapp.project.user.infrastructure;
2. Dependency Management
Always inherit from spring-boot-starter-parent or use spring-boot-dependencies BOM for consistent dependency versions.
✅ GOOD: Maven pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.11</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3. Dependency Injection
Prefer constructor injection for all dependencies. This ensures immutability, simplifies testing, and makes dependencies explicit. Use Lombok's @RequiredArgsConstructor for conciseness.
❌ BAD: Field Injection
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
✅ GOOD: Constructor Injection with Lombok
package com.myapp.project.user.domain;
import com.myapp.project.user.infrastructure.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User findUserById(Long id) {
return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException(id));
}
}
4. API Design and Controllers
Controllers must be stateless and focused solely on routing HTTP requests and responses. Delegate all business logic to service layers. Use DTOs for request/response bodies.
❌ BAD: Business Logic in Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
return ResponseEntity.ok(userRepository.save(user));
}
}
✅ GOOD: Lean Controller, Delegate to Service
package com.myapp.project.user.api;
import com.myapp.project.user.domain.UserService;
import com.myapp.project.user.api.dto.UserCreateRequest;
import com.myapp.project.user.api.dto.UserResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserResponse> createUser(@RequestBody UserCreateRequest request) {
UserResponse response = userService.createUser(request);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
UserResponse response = userService.findUserById(id);
return ResponseEntity.ok(response);
}
}
5. Logging
Use SLF4J with Logback (Spring Boot's default) for all logging. Avoid System.out.print(). Use Lombok's @Slf4j for convenience. Avoid string concatenation in log messages; use parameterized logging.
❌ BAD: System.out.print() and String Concatenation
public void process(String data) {
System.out.println("Processing data: " + data);
log.info("Processing data: " + data);
}
✅ GOOD: Parameterized SLF4J Logging
package com.myapp.project.user.domain;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserService {
public void processData(String data) {
log.info("Processing data: {}", data);
try {
} catch (Exception e) {
log.error("Failed to process data: {}", data, e);
}
}
}
6. Error Handling
Implement global exception handling using @RestControllerAdvice to provide consistent and meaningful error responses.
✅ GOOD: Global Exception Handler
package com.myapp.project.common.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Object> handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
record ErrorResponse {}
}
com.myapp.project.user.domain;
org.springframework.http.HttpStatus;
org.springframework.web.bind.annotation.ResponseStatus;
{
{
( + id);
}
}
7. Configuration
Store all external configuration in application.yml or environment variables. Avoid hardcoding values. Use @ConfigurationProperties for type-safe configuration.
✅ GOOD: application.yml
app:
service:
baseUrl: https://api.example.com
timeoutMs: 5000
✅ GOOD: Type-Safe Configuration Class
package com.myapp.project.common.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "app.service")
@Data
public class AppServiceProperties {
private String baseUrl;
private int timeoutMs;
}
@Service
@RequiredArgsConstructor
public class ExternalServiceClient {
private final AppServiceProperties properties;
public void callExternalService() {
}
}
8. Testing
Write comprehensive unit and integration tests. Leverage Spring Boot's testing utilities. Constructor injection greatly aids unit testing.
✅ GOOD: Unit Test (Service Layer)
package com.myapp.project.user.domain;
import com.myapp.project.user.infrastructure.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void findUserById_userExists_returnsUser() {
User mockUser = new User(1L, "test@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
User foundUser = userService.findUserById(1L);
}
@Test
void findUserById_userNotFound_throwsException {
(userRepository.findById()).thenReturn(Optional.empty());
assertThrows(UserNotFoundException.class, () -> userService.findUserById());
}
}
✅ GOOD: Integration Test (Controller Layer)
package com.myapp.project.user.api;
import com.myapp.project.user.domain.UserService;
import com.myapp.project.user.api.dto.UserCreateRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(UserController.class)
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void createUser_validRequest_returnsCreated() throws Exception {
UserCreateRequest request = new UserCreateRequest("newuser@example.com", );
(userService.createUser(any(UserCreateRequest.class)))
.thenReturn( (, ));
mockMvc.perform(post()
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated());
}
}