Back to blog

Spring Boot REST APIs: Advanced Patterns

javaspring-bootrest-apibackendapi-design
Spring Boot REST APIs: Advanced Patterns

Introduction

In our previous post on Getting Started with Spring Boot, we built a simple REST API. Now, let's take it to the next level by exploring advanced patterns and best practices used in production environments.

What You'll Learn

✅ Implement robust exception handling and error responses
✅ Add request validation with Bean Validation
✅ Build pagination and filtering for large datasets
✅ Implement API versioning strategies
✅ Secure your API with Spring Security and JWT
✅ Generate OpenAPI/Swagger documentation
✅ Write comprehensive tests (Unit, Integration, E2E)
✅ Optimize API performance

Prerequisites

  • Java 17 or higher installed
  • Understanding of Spring Boot basics (see our getting started guide)
  • Familiarity with REST API principles
  • Maven or Gradle experience

1. Advanced Exception Handling

Global Exception Handler

Spring Boot's @ControllerAdvice allows centralized exception handling across all controllers.

@ControllerAdvice
public class GlobalExceptionHandler {
 
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(
            ResourceNotFoundException ex,
            WebRequest request) {
 
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now(),
            request.getDescription(false)
        );
 
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationErrors(
            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,
            LocalDateTime.now()
        );
 
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }
 
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(
            Exception ex,
            WebRequest request) {
 
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "An unexpected error occurred",
            LocalDateTime.now(),
            request.getDescription(false)
        );
 
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Custom Error Response

@Data
@AllArgsConstructor
public class ErrorResponse {
    private int status;
    private String message;
    private LocalDateTime timestamp;
    private String path;
    private Map<String, String> errors; // For validation errors
}

Custom Exceptions

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String resource, String field, Object value) {
        super(String.format("%s not found with %s: '%s'", resource, field, value));
    }
}
 
public class BadRequestException extends RuntimeException {
    public BadRequestException(String message) {
        super(message);
    }
}

Usage in Controller:

@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    User user = userRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
 
    return ResponseEntity.ok(user);
}

2. Request Validation with Bean Validation

Adding Dependencies

<!-- Already included in spring-boot-starter-web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Entity Validation

@Entity
@Table(name = "users")
@Data
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
    private String username;
 
    @NotBlank(message = "Email is required")
    @Email(message = "Email should be valid")
    private String email;
 
    @NotBlank(message = "Password is required")
    @Size(min = 8, message = "Password must be at least 8 characters")
    @Pattern(
        regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).*$",
        message = "Password must contain uppercase, lowercase, digit, and special character"
    )
    private String password;
 
    @Min(value = 18, message = "Age must be at least 18")
    @Max(value = 120, message = "Age must be less than 120")
    private Integer age;
 
    @Past(message = "Birth date must be in the past")
    private LocalDate birthDate;
}

DTO Validation

@Data
public class CreateUserRequest {
 
    @NotBlank(message = "Username is required")
    @Size(min = 3, max = 50)
    private String username;
 
    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    private String email;
 
    @NotBlank(message = "Password is required")
    @Size(min = 8, max = 100)
    private String password;
 
    @NotNull(message = "Age is required")
    @Min(18)
    @Max(120)
    private Integer age;
}

Controller Validation

@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
 
    @PostMapping
    public ResponseEntity<User> createUser(
            @Valid @RequestBody CreateUserRequest request) {
 
        User user = userService.createUser(request);
        return new ResponseEntity<>(user, HttpStatus.CREATED);
    }
 
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(
            @PathVariable @Min(1) Long id) {
 
        User user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }
}

Custom Validators

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
    String message() default "Email already exists";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
 
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
 
    @Autowired
    private UserRepository userRepository;
 
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        return email != null && !userRepository.existsByEmail(email);
    }
}

3. Pagination and Filtering

Pagination with Spring Data

@RestController
@RequestMapping("/api/v1/users")
public class UserController {
 
    @Autowired
    private UserService userService;
 
    @GetMapping
    public ResponseEntity<Page<UserDTO>> getUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDir) {
 
        Sort.Direction direction = sortDir.equalsIgnoreCase("desc")
            ? Sort.Direction.DESC
            : Sort.Direction.ASC;
 
        Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy));
        Page<UserDTO> users = userService.getUsers(pageable);
 
        return ResponseEntity.ok(users);
    }
}

Custom Pagination Response

@Data
@AllArgsConstructor
public class PageResponse<T> {
    private List<T> content;
    private int pageNumber;
    private int pageSize;
    private long totalElements;
    private int totalPages;
    private boolean last;
    private boolean first;
}
 
// In service layer
public PageResponse<UserDTO> getUsers(Pageable pageable) {
    Page<User> page = userRepository.findAll(pageable);
    List<UserDTO> content = page.getContent().stream()
        .map(this::convertToDTO)
        .collect(Collectors.toList());
 
    return new PageResponse<>(
        content,
        page.getNumber(),
        page.getSize(),
        page.getTotalElements(),
        page.getTotalPages(),
        page.isLast(),
        page.isFirst()
    );
}

Advanced Filtering with Specifications

public class UserSpecification {
 
    public static Specification<User> hasUsername(String username) {
        return (root, query, cb) ->
            username == null ? null : cb.like(root.get("username"), "%" + username + "%");
    }
 
    public static Specification<User> hasEmail(String email) {
        return (root, query, cb) ->
            email == null ? null : cb.like(root.get("email"), "%" + email + "%");
    }
 
    public static Specification<User> ageGreaterThan(Integer age) {
        return (root, query, cb) ->
            age == null ? null : cb.greaterThanOrEqualTo(root.get("age"), age);
    }
 
    public static Specification<User> ageLessThan(Integer age) {
        return (root, query, cb) ->
            age == null ? null : cb.lessThanOrEqualTo(root.get("age"), age);
    }
}
 
// Repository
public interface UserRepository extends JpaRepository<User, Long>,
                                        JpaSpecificationExecutor<User> {
}
 
// Controller
@GetMapping("/search")
public ResponseEntity<Page<UserDTO>> searchUsers(
        @RequestParam(required = false) String username,
        @RequestParam(required = false) String email,
        @RequestParam(required = false) Integer minAge,
        @RequestParam(required = false) Integer maxAge,
        Pageable pageable) {
 
    Specification<User> spec = Specification
        .where(UserSpecification.hasUsername(username))
        .and(UserSpecification.hasEmail(email))
        .and(UserSpecification.ageGreaterThan(minAge))
        .and(UserSpecification.ageLessThan(maxAge));
 
    Page<User> users = userRepository.findAll(spec, pageable);
    // Convert to DTO and return
}

4. API Versioning Strategies

@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
    // Version 1 implementation
}
 
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
    // Version 2 implementation with breaking changes
}

Pros: Clear, easy to understand, widely used Cons: URL pollution, duplicate code

Strategy 2: Header Versioning

@RestController
@RequestMapping("/api/users")
public class UserController {
 
    @GetMapping(headers = "API-Version=1")
    public ResponseEntity<UserDTOV1> getUserV1(@PathVariable Long id) {
        // Version 1 logic
    }
 
    @GetMapping(headers = "API-Version=2")
    public ResponseEntity<UserDTOV2> getUserV2(@PathVariable Long id) {
        // Version 2 logic
    }
}

Request:

curl -H "API-Version: 2" http://localhost:8080/api/users/1

Strategy 3: Content Negotiation

@GetMapping(produces = "application/vnd.company.v1+json")
public ResponseEntity<UserDTOV1> getUserV1(@PathVariable Long id) {
    // Version 1 logic
}
 
@GetMapping(produces = "application/vnd.company.v2+json")
public ResponseEntity<UserDTOV2> getUserV2(@PathVariable Long id) {
    // Version 2 logic
}

Request:

curl -H "Accept: application/vnd.company.v2+json" http://localhost:8080/api/users/1

5. Security with Spring Security and JWT

Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

JWT Utility Class

@Component
public class JwtUtil {
 
    @Value("${jwt.secret}")
    private String secret;
 
    @Value("${jwt.expiration}")
    private Long expiration;
 
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }
 
    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(subject)
            .setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(getSignKey(), SignatureAlgorithm.HS256)
            .compact();
    }
 
    private Key getSignKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
 
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
 
    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
 
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
 
    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(getSignKey())
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
 
    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
 
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

JWT Authentication Filter

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
    @Autowired
    private JwtUtil jwtUtil;
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
 
        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String username;
 
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
 
        jwt = authHeader.substring(7);
        username = jwtUtil.extractUsername(jwt);
 
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
 
            if (jwtUtil.validateToken(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                    );
 
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
 
        filterChain.doFilter(request, response);
    }
}

Security Configuration

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
 
    @Autowired
    private JwtAuthenticationFilter jwtAuthFilter;
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/api/v1/public/**").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authenticationProvider(authenticationProvider())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
 
        return http.build();
    }
 
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
            throws Exception {
        return config.getAuthenticationManager();
    }
}

Authentication Controller

@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
 
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @Autowired
    private JwtUtil jwtUtil;
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @PostMapping("/login")
    public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getUsername(),
                request.getPassword()
            )
        );
 
        UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
        String token = jwtUtil.generateToken(userDetails);
 
        return ResponseEntity.ok(new AuthResponse(token));
    }
 
    @PostMapping("/register")
    public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
        // User registration logic
        // Hash password with passwordEncoder.encode()
        // Save user to database
        // Generate token
        // Return token
    }
}

6. OpenAPI/Swagger Documentation

Dependencies

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>

Configuration

@Configuration
public class OpenAPIConfig {
 
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("User Management API")
                .version("1.0")
                .description("REST API for managing users with Spring Boot")
                .contact(new Contact()
                    .name("Chanh Le")
                    .email("contact@chanhle.dev")
                    .url("https://chanhle.dev")
                )
                .license(new License()
                    .name("Apache 2.0")
                    .url("https://www.apache.org/licenses/LICENSE-2.0.html")
                )
            )
            .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
            .components(new Components()
                .addSecuritySchemes("bearerAuth", new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")
                )
            );
    }
}

Controller Documentation

@RestController
@RequestMapping("/api/v1/users")
@Tag(name = "User Management", description = "APIs for managing users")
public class UserController {
 
    @Operation(
        summary = "Get user by ID",
        description = "Returns a single user by their ID"
    )
    @ApiResponses(value = {
        @ApiResponse(
            responseCode = "200",
            description = "User found",
            content = @Content(
                mediaType = "application/json",
                schema = @Schema(implementation = UserDTO.class)
            )
        ),
        @ApiResponse(
            responseCode = "404",
            description = "User not found",
            content = @Content(
                mediaType = "application/json",
                schema = @Schema(implementation = ErrorResponse.class)
            )
        ),
        @ApiResponse(responseCode = "401", description = "Unauthorized")
    })
    @SecurityRequirement(name = "bearerAuth")
    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUserById(
            @Parameter(description = "User ID", required = true)
            @PathVariable Long id) {
 
        UserDTO user = userService.getUserById(id);
        return ResponseEntity.ok(user);
    }
 
    @Operation(
        summary = "Create new user",
        description = "Creates a new user and returns the created user"
    )
    @ApiResponses(value = {
        @ApiResponse(
            responseCode = "201",
            description = "User created successfully"
        ),
        @ApiResponse(
            responseCode = "400",
            description = "Invalid input"
        )
    })
    @PostMapping
    public ResponseEntity<UserDTO> createUser(
            @io.swagger.v3.oas.annotations.parameters.RequestBody(
                description = "User object to be created",
                required = true,
                content = @Content(
                    schema = @Schema(implementation = CreateUserRequest.class)
                )
            )
            @Valid @RequestBody CreateUserRequest request) {
 
        UserDTO user = userService.createUser(request);
        return new ResponseEntity<>(user, HttpStatus.CREATED);
    }
}

Access Swagger UI: http://localhost:8080/swagger-ui.html


7. Testing Strategies

Unit Tests

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
 
    @Mock
    private UserRepository userRepository;
 
    @InjectMocks
    private UserService userService;
 
    @Test
    public void testGetUserById_Success() {
        // Arrange
        Long userId = 1L;
        User user = new User();
        user.setId(userId);
        user.setUsername("testuser");
 
        when(userRepository.findById(userId)).thenReturn(Optional.of(user));
 
        // Act
        UserDTO result = userService.getUserById(userId);
 
        // Assert
        assertNotNull(result);
        assertEquals(userId, result.getId());
        assertEquals("testuser", result.getUsername());
        verify(userRepository, times(1)).findById(userId);
    }
 
    @Test
    public void testGetUserById_NotFound() {
        // Arrange
        Long userId = 999L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());
 
        // Act & Assert
        assertThrows(ResourceNotFoundException.class, () -> {
            userService.getUserById(userId);
        });
    }
}

Integration Tests

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserControllerIntegrationTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Autowired
    private ObjectMapper objectMapper;
 
    @Test
    @Order(1)
    public void testCreateUser() throws Exception {
        CreateUserRequest request = new CreateUserRequest();
        request.setUsername("testuser");
        request.setEmail("test@example.com");
        request.setPassword("Test@1234");
        request.setAge(25);
 
        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.username").value("testuser"))
            .andExpect(jsonPath("$.email").value("test@example.com"));
    }
 
    @Test
    @Order(2)
    public void testGetUserById() throws Exception {
        mockMvc.perform(get("/api/v1/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.username").isNotEmpty());
    }
 
    @Test
    public void testGetUserById_NotFound() throws Exception {
        mockMvc.perform(get("/api/v1/users/999"))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.message").value("User not found with id: '999'"));
    }
}

Repository Tests

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserRepositoryTest {
 
    @Autowired
    private UserRepository userRepository;
 
    @Test
    public void testFindByEmail_Success() {
        // Arrange
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        user.setPassword("password");
        user.setAge(25);
 
        userRepository.save(user);
 
        // Act
        Optional<User> found = userRepository.findByEmail("test@example.com");
 
        // Assert
        assertTrue(found.isPresent());
        assertEquals("testuser", found.get().getUsername());
    }
 
    @Test
    public void testExistsByEmail() {
        // Arrange
        User user = new User();
        user.setEmail("test@example.com");
        userRepository.save(user);
 
        // Act
        boolean exists = userRepository.existsByEmail("test@example.com");
 
        // Assert
        assertTrue(exists);
    }
}

Test Configuration

# src/test/resources/application-test.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
 
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true
 
  h2:
    console:
      enabled: true
 
jwt:
  secret: testsecretkeytestsecretkeytestsecretkey
  expiration: 3600000

8. Performance Optimization

Caching with Spring Cache

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
@Configuration
@EnableCaching
public class CacheConfig {
 
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users", "posts");
    }
}
 
@Service
public class UserService {
 
    @Cacheable(value = "users", key = "#id")
    public UserDTO getUserById(Long id) {
        // Database call - cached after first call
    }
 
    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        // Invalidates cache on delete
    }
 
    @CachePut(value = "users", key = "#result.id")
    public UserDTO updateUser(Long id, UpdateUserRequest request) {
        // Updates cache with new value
    }
}

Database Query Optimization

// N+1 Query Problem - BAD
@OneToMany(mappedBy = "user")
private List<Post> posts;
 
// Solution 1: JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.posts WHERE u.id = :id")
User findByIdWithPosts(@Param("id") Long id);
 
// Solution 2: Entity Graph
@EntityGraph(attributePaths = {"posts", "comments"})
Optional<User> findById(Long id);
 
// Solution 3: Batch Fetching
@BatchSize(size = 10)
@OneToMany(mappedBy = "user")
private List<Post> posts;

Connection Pool Tuning

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

Async Processing

@Configuration
@EnableAsync
public class AsyncConfig {
 
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}
 
@Service
public class EmailService {
 
    @Async
    public CompletableFuture<Void> sendWelcomeEmail(String email) {
        // Send email asynchronously
        return CompletableFuture.completedFuture(null);
    }
}

9. Production Best Practices

Health Checks and Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always
  metrics:
    export:
      prometheus:
        enabled: true

Logging Configuration

logging:
  level:
    root: INFO
    com.example.api: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
  file:
    name: /var/log/app.log
    max-size: 10MB
    max-history: 30

CORS Configuration

@Configuration
public class CorsConfig {
 
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**")
                    .allowedOrigins("https://yourdomain.com")
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
                    .allowedHeaders("*")
                    .allowCredentials(true)
                    .maxAge(3600);
            }
        };
    }
}

Rate Limiting

<dependency>
    <groupId>com.github.vladimir-bukhtoyarov</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>7.6.0</version>
</dependency>
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
 
    private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
 
    @Override
    public boolean preHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) throws Exception {
 
        String key = request.getRemoteAddr();
        Bucket bucket = resolveBucket(key);
 
        if (bucket.tryConsume(1)) {
            return true;
        }
 
        response.setStatus(429); // Too Many Requests
        response.getWriter().write("Rate limit exceeded");
        return false;
    }
 
    private Bucket resolveBucket(String key) {
        return cache.computeIfAbsent(key, k -> {
            Bandwidth limit = Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)));
            return Bucket.builder()
                .addLimit(limit)
                .build();
        });
    }
}

10. Complete Example Project Structure

src/
├── main/
│   ├── java/
│   │   └── com/example/api/
│   │       ├── config/
│   │       │   ├── SecurityConfig.java
│   │       │   ├── OpenAPIConfig.java
│   │       │   ├── CacheConfig.java
│   │       │   └── AsyncConfig.java
│   │       ├── controller/
│   │       │   ├── UserController.java
│   │       │   └── AuthController.java
│   │       ├── dto/
│   │       │   ├── UserDTO.java
│   │       │   ├── CreateUserRequest.java
│   │       │   ├── UpdateUserRequest.java
│   │       │   └── AuthResponse.java
│   │       ├── entity/
│   │       │   └── User.java
│   │       ├── exception/
│   │       │   ├── GlobalExceptionHandler.java
│   │       │   ├── ResourceNotFoundException.java
│   │       │   └── ErrorResponse.java
│   │       ├── filter/
│   │       │   └── JwtAuthenticationFilter.java
│   │       ├── repository/
│   │       │   └── UserRepository.java
│   │       ├── service/
│   │       │   ├── UserService.java
│   │       │   └── UserServiceImpl.java
│   │       ├── specification/
│   │       │   └── UserSpecification.java
│   │       ├── util/
│   │       │   └── JwtUtil.java
│   │       └── ApiApplication.java
│   └── resources/
│       ├── application.yml
│       └── application-prod.yml
└── test/
    ├── java/
    │   └── com/example/api/
    │       ├── controller/
    │       │   └── UserControllerTest.java
    │       ├── service/
    │       │   └── UserServiceTest.java
    │       └── repository/
    │           └── UserRepositoryTest.java
    └── resources/
        └── application-test.yml

Summary and Key Takeaways

In this comprehensive guide, we've covered advanced patterns for building production-ready REST APIs with Spring Boot:

Exception Handling: Centralized error handling with @ControllerAdvice
Validation: Bean Validation with custom validators
Pagination: Spring Data pagination and filtering with Specifications
API Versioning: URI, header, and content negotiation strategies
Security: JWT authentication with Spring Security
Documentation: OpenAPI/Swagger integration
Testing: Unit, integration, and repository tests
Performance: Caching, query optimization, async processing
Production: Health checks, logging, CORS, rate limiting

Next Steps

  1. Implement these patterns in your existing Spring Boot projects
  2. Add monitoring with Prometheus and Grafana
  3. Deploy to production with Docker and Kubernetes
  4. Explore microservices architecture with Spring Cloud

Questions or feedback? Feel free to reach out at contact@chanhle.dev or connect with me on X.

Happy coding! 🚀

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.