Spring Best Practices
This guide outlines essential best practices for developing with Spring Boot, ensuring your applications are well-structured, performant, secure, and easily testable.
1. Code Organization and Structure
Maintain a clean, logical package structure.
1.1. Base Package and @SpringBootApplication
Place your main application class at the root of a well-named base package. All components should reside within this base package or its sub-packages.
❌ BAD:
// com.example.Application.java
package com.example; // Too generic, or in default package
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
✅ GOOD:
// com.company.project.MyApplication.java
package com.company.project; // Specific base package
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication { // Clear application entry point
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
1.2. Configuration Classes
Group related bean definitions into dedicated @Configuration classes.
❌ BAD:
// MyApplication.java (monolithic config)
@SpringBootApplication
public class MyApplication {
// ...
@Bean
public DataSource dataSource() { /* ... */ }
@Bean
public RestTemplate restTemplate() { /* ... */ }
}
✅ GOOD:
// com.company.project.config.DataSourceConfig.java
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() { /* ... */ }
}
// com.company.project.config.WebClientConfig.java
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder.baseUrl("http://api.example.com").build();
}
}
2. Common Patterns and Anti-patterns
2.1. Constructor Injection
Always prefer constructor injection for mandatory dependencies. It ensures immutability and testability.
❌ BAD:
@Service
public class UserService {
@Autowired // Field injection is discouraged
private UserRepository userRepository;
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
✅ GOOD:
@Service
public class UserService {
private final UserRepository userRepository; // Immutable
@Autowired // Optional for single constructor in Spring 4.3+
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
2.2. Immutable Data Models
Use immutable classes for DTOs and entities where possible, especially for value objects.
❌ BAD:
public class UserDto { // Mutable
private Long id;
private String name;
// Getters and Setters
}
✅ GOOD:
public record UserDto(Long id, String name) {} // Immutable record (Java 16+)
// Or with Lombok:
@Value // All-args constructor, getters, equals, hashCode, toString
public class UserDto {
Long id;
String name;
}
3. Performance Considerations
3.1. N+1 Query Problem
Avoid N+1 queries by eagerly fetching related data when necessary.
❌ BAD:
@Service
public class OrderService {
@Autowired private OrderRepository orderRepository;
public List<Order> getOrdersWithItems() {
List<Order> orders = orderRepository.findAll();
orders.forEach(order -> order.getItems().size()); // N+1 queries for items
return orders;
}
}
✅ GOOD:
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.items") // Eagerly fetch items
List<Order> findAllWithItems();
}
@Service
public class OrderService {
@Autowired private OrderRepository orderRepository;
public List<Order> getOrdersWithItems() {
return orderRepository.findAllWithItems(); // Single query
}
}
4. Common Pitfalls and Gotchas
4.1. Disabling Auto-configuration
Only disable auto-configuration when strictly necessary to avoid unexpected behavior.
❌ BAD:
// Always disabling specific auto-configs without understanding impact
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public class MyApplication { /* ... */ }
✅ GOOD:
// Only disable when you provide your own explicit configuration
// and the auto-config conflicts or is unwanted.
// For example, if you manage DataSource manually:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MyApplication { /* ... */ }
5. Security Best Practices
5.1. Input Validation
Always validate user input at the API boundary. Use Spring's validation features.
❌ BAD:
@PostMapping("/users")
public ResponseEntity<UserDto> createUser(@RequestBody UserDto userDto) {
// No validation, directly use userDto
userService.save(userDto);
return ResponseEntity.ok(userDto);
}
✅ GOOD:
@PostMapping("/users")
public ResponseEntity<UserDto> createUser(@Valid @RequestBody UserDto userDto) {
// @Valid triggers validation based on JSR-303 annotations in UserDto
userService.save(userDto);
return ResponseEntity.ok(userDto);
}
// In UserDto:
public record UserDto(@NotNull Long id, @NotBlank @Size(min = 2, max = 50) String name) {}
6. Error Handling
6.1. Global Exception Handling
Use @ControllerAdvice for consistent global error handling.
❌ BAD:
@RestController
public class UserController {
// ...
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
try {
return userService.findById(id);
} catch (UserNotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found", e);
}
}
}
✅ GOOD:
@RestControllerAdvice // Global exception handler
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleUserNotFound(UserNotFoundException ex) {
return new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
}
}
// In UserController:
@RestController
public class UserController {
// ...
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id); // Let exception propagate
}
}
// Custom exception:
public class UserNotFoundException extends RuntimeException { /* ... */ }
public record ErrorResponse(String code, String message) {}
7. API Design
7.1. RESTful Principles and DTOs
Design REST APIs following standard principles and use DTOs to decouple internal models from external API contracts.
❌ BAD:
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired private ProductService productService;
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) { // Exposing internal entity
return productService.findById(id);
}
}
✅ GOOD:
@RestController
@RequestMapping("/api/v1/products") // Versioned API path
public class ProductController {
@Autowired private ProductService productService;
@GetMapping("/{id}")
public ProductResponseDto getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
return ProductMapper.toDto(product); // Map to DTO
}
}
public record ProductResponseDto(Long id, String name, BigDecimal price) {}
// ProductMapper is a utility to convert Product entity to ProductResponseDto
8. Testing Approaches
8.1. Layered Testing
Use specific Spring Boot test annotations for different layers.
❌ BAD:
@SpringBootTest // Heavy for simple unit tests
public class UserServiceTest {
@Autowired private UserService userService;
// ...
}
✅ GOOD:
// Unit test for service layer (mocking dependencies)
@ExtendWith(MockitoExtension.class)
public class UserServiceUnitTest {
@Mock private UserRepository userRepository;
@InjectMocks private UserService userService;
@Test
void findById_shouldReturnUser() {
// ...
}
}
// Integration test for web layer (no full context)
@WebMvcTest(UserController.class)
public class UserControllerWebMvcTest {
@Autowired private MockMvc mockMvc;
@MockBean private UserService userService; // Mock service dependency
@Test
void getUser_shouldReturnOk() throws Exception {
// ...
}
}
// Integration test for data layer
@DataJpaTest
public class UserRepositoryIntegrationTest {
@Autowired private TestEntityManager entityManager;
@Autowired private UserRepository userRepository;
@Test
void findById_shouldReturnProduct() {
// ...
}
}
// Full application context test (use sparingly)
@SpringBootTest
@AutoConfigureMockMvc
public class MyApplicationIntegrationTest {
@Autowired private MockMvc mockMvc;
// ...
}