Spring Boot Best Practices: Building Production-Ready Applications
Spring Boot is the most popular Java framework for building enterprise applications. Here are best practices for building production-ready Spring Boot applications. 1. Project Structure Recommended Structure src/ ├── main/ │ ├── java/ │ │ └── com/example/ │ │ ├── Application.java │ │ ├── config/ │ │ ├── controller/ │ │ ├── service/ │ │ ├── repository/ │ │ ├── model/ │ │ └── dto/ │ └── resources/ │ ├── application.yml │ └── application-prod.yml └── test/ 2. Configuration Management Use YAML for Configuration # application.yml spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: ${DB_USERNAME} password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate show-sql: false properties: hibernate: format_sql: true server: port: 8080 error: include-message: always include-stacktrace: on_param Profile-Based Configuration # application-dev.yml spring: datasource: url: jdbc:h2:mem:testdb jpa: show-sql: true # application-prod.yml spring: datasource: url: ${DATABASE_URL} jpa: show-sql: false 3. Dependency Injection Constructor Injection // Good: Constructor injection @Service public class UserService { private final UserRepository userRepository; private final EmailService emailService; public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository = userRepository; this.emailService = emailService; } } // Bad: Field injection @Service public class UserService { @Autowired private UserRepository userRepository; } 4. Exception Handling Global Exception Handler @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound( ResourceNotFoundException ex) { ErrorResponse error = new ErrorResponse( HttpStatus.NOT_FOUND.value(), ex.getMessage() ); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidation( MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()) ); ErrorResponse error = new ErrorResponse( HttpStatus.BAD_REQUEST.value(), "Validation failed", errors ); return ResponseEntity.badRequest().body(error); } } 5. REST API Design Controller Best Practices @RestController @RequestMapping("/api/v1/users") @Validated public class UserController { private final UserService userService; @GetMapping public ResponseEntity<List<UserDTO>> getAllUsers( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { List<UserDTO> users = userService.getAllUsers(page, size); return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity<UserDTO> getUser(@PathVariable Long id) { UserDTO user = userService.getUserById(id); return ResponseEntity.ok(user); } @PostMapping public ResponseEntity<UserDTO> createUser( @Valid @RequestBody CreateUserRequest request) { UserDTO user = userService.createUser(request); return ResponseEntity.status(HttpStatus.CREATED).body(user); } @PutMapping("/{id}") public ResponseEntity<UserDTO> updateUser( @PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { UserDTO user = userService.updateUser(id, request); return ResponseEntity.ok(user); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.noContent().build(); } } 6. Service Layer Service Implementation @Service @Transactional public class UserService { private final UserRepository userRepository; private final UserMapper userMapper; public UserDTO getUserById(Long id) { User user = userRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException( "User not found with id: " + id)); return userMapper.toDTO(user); } public UserDTO createUser(CreateUserRequest request) { if (userRepository.existsByEmail(request.getEmail())) { throw new DuplicateResourceException( "Email already exists"); } User user = userMapper.toEntity(request); user = userRepository.save(user); return userMapper.toDTO(user); } } 7. Repository Layer JPA Repository @Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); boolean existsByEmail(String email); @Query("SELECT u FROM User u WHERE u.status = :status") List<User> findByStatus(@Param("status") UserStatus status); @Modifying @Query("UPDATE User u SET u.status = :status WHERE u.id = :id") int updateStatus(@Param("id") Long id, @Param("status") UserStatus status); } 8. Entity Design JPA Entity @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String name; @Enumerated(EnumType.STRING) private UserStatus status; @CreatedDate private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime updatedAt; @Version private Long version; // Getters and setters } 9. Validation DTO Validation public class CreateUserRequest { @NotBlank(message = "Email is required") @Email(message = "Invalid email format") private String email; @NotBlank(message = "Name is required") @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") private String name; @Min(value = 18, message = "Age must be at least 18") @Max(value = 120, message = "Age must be at most 120") private Integer age; // Getters and setters } 10. Security Spring Security Configuration @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.decoder(jwtDecoder())) ); return http.build(); } } 11. Testing Unit Tests @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void shouldCreateUser() { // Given CreateUserRequest request = new CreateUserRequest(); request.setEmail("[email protected]"); request.setName("Test User"); User savedUser = new User(); savedUser.setId(1L); savedUser.setEmail(request.getEmail()); when(userRepository.existsByEmail(request.getEmail())).thenReturn(false); when(userRepository.save(any(User.class))).thenReturn(savedUser); // When UserDTO result = userService.createUser(request); // Then assertThat(result.getId()).isEqualTo(1L); assertThat(result.getEmail()).isEqualTo("[email protected]"); } } Integration Tests @SpringBootTest @AutoConfigureMockMvc class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private UserRepository userRepository; @Test void shouldCreateUser() throws Exception { CreateUserRequest request = new CreateUserRequest(); request.setEmail("[email protected]"); request.setName("Test User"); mockMvc.perform(post("/api/v1/users") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.email").value("[email protected]")); } } 12. Performance Optimization Connection Pooling spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 Caching @Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("users", "posts"); } } // Usage @Service public class UserService { @Cacheable(value = "users", key = "#id") public UserDTO getUserById(Long id) { return userRepository.findById(id) .map(userMapper::toDTO) .orElseThrow(); } @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { userRepository.deleteById(id); } } 13. Monitoring and Logging Actuator Endpoints management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: when-authorized Logging Configuration logging: level: root: INFO com.example: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" file: name: logs/application.log max-size: 10MB max-history: 30 Best Practices Summary Use constructor injection for dependencies Implement global exception handling Validate all inputs with Bean Validation Use DTOs to separate API from domain models Implement proper logging and monitoring Use profiles for environment-specific config Write comprehensive tests Optimize database queries Implement caching where appropriate Follow RESTful conventions Conclusion Spring Boot best practices help you build: ...