Master Spring Boot Controllers: Design Patterns, Best Practices & Code Samples

This comprehensive guide explains how to design high‑quality Spring Boot controller layers, covering architecture planning, RESTful API conventions, parameter validation, unified response structures, exception handling, logging, security, testing, asynchronous processing, and performance optimization with practical code examples and clear best‑practice recommendations.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Master Spring Boot Controllers: Design Patterns, Best Practices & Code Samples

Introduction

In a Spring Boot application the Controller layer acts as the "facade" that directly interacts with clients; its design quality directly impacts the overall usability, maintainability, and scalability of the application.

This article systematically introduces how to plan and write high‑quality Controller code, covering RESTful design, parameter handling, exception handling, logging, security control, and provides concrete code examples and architectural suggestions.

1. Basic Architecture Planning for the Controller Layer

1.1 Layer Responsibility Division

In Spring Boot, a typical Controller should follow the "thin controller" principle. Main responsibilities include:

Request routing: map HTTP requests to handling methods

Parameter handling: receive, validate, and convert request parameters

Response handling: encapsulate and return a unified response format

Exception capture: handle business and system exceptions

Cross‑cutting concerns: logging, authentication, rate limiting, etc.

1.2 Package Structure Planning

It is recommended to organize packages by functional modules to avoid placing all Controllers in a single package:

com.example.app
├── config/                # configuration classes
├── controller/
│   ├── v1/               # API version 1
│   │   ├── UserController.java
│   │   └── ProductController.java
│   ├── v2/               # API version 2
│   └── admin/            # admin interfaces
├── service/               # business logic layer
├── repository/           # data access layer
└── model/                 # data models

1.3 Unified Response Format

Define a standard response body to keep API consistency:

public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    private long timestamp;

    // success response
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data);
    }

    // failure response
    public static <T> ApiResponse<T> fail(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }

    // constructors, getters, setters omitted
}

2. RESTful API Design Guidelines

2.1 Resource Naming and HTTP Methods

图片
图片

2.2 Versioning Strategy

URL path versioning (recommended):

@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 { /* v1 endpoints */ }

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 { /* v2 endpoints */ }

Request‑header versioning:

@GetMapping(value = "/users", headers = "X-API-VERSION=1")
public ApiResponse<List<User>> getUsersV1() { ... }

@GetMapping(value = "/users", headers = "X-API-VERSION=2")
public ApiResponse<List<UserDto>> getUsersV2() { ... }

2.3 Status Code Guidelines

Common HTTP status codes: 200 OK – successful GET request 201 Created – resource created successfully 204 No Content – successful request with no body 400 Bad Request – request parameter error 401 Unauthorized – unauthenticated 403 Forbidden – insufficient permissions 404 Not Found – resource does not exist 500 Internal Server Error – server error

3. Best Practices for Request Parameter Handling

3.1 Parameter Reception Method Selection

图片
图片

3.2 Parameter Validation Scheme

Use JSR‑303 validation with Hibernate Validator:

@PostMapping("/users")
public ApiResponse<User> createUser(@Valid @RequestBody UserCreateRequest request) {
    // business logic
}

public class UserCreateRequest {
    @NotBlank(message = "Username cannot be empty")
    @Size(min = 4, max = 20, message = "Username length must be 4‑20 characters")
    private String username;

    @Email(message = "Invalid email format")
    private String email;

    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", message = "Password must be at least 8 characters with letters and numbers")
    private String password;

    @NotNull(message = "Age cannot be null")
    @Min(value = 18, message = "Age must be greater than 18")
    private Integer age;

    // getters/setters omitted
}

3.3 Custom Parameter Resolution

Implement HandlerMethodArgumentResolver for special parameters:

public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        String token = request.getHeader("Authorization");
        return authService.getUserByToken(token);
    }
}

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentUserArgumentResolver());
    }
}

@GetMapping("/profile")
public ApiResponse<UserProfile> getProfile(@CurrentUser User user) {
    return ApiResponse.success(userService.getProfile(user.getId()));
}

4. Response Handling and Exception Management

4.1 Unified Response Wrapping

@RestControllerAdvice
public class ResponseWrapper implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return !returnType.getParameterType().equals(ApiResponse.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof String) {
            return JsonUtils.toJson(ApiResponse.success(body));
        }
        return ApiResponse.success(body);
    }
}

4.2 Global Exception Handling

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(BusinessException.class)
    public ApiResponse<Void> handleBusinessException(BusinessException e) {
        logger.warn("Business exception: {}", e.getMessage());
        return ApiResponse.fail(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getAllErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining("; "));
        return ApiResponse.fail(400, message);
    }

    @ExceptionHandler(Exception.class)
    public ApiResponse<Void> handleException(Exception e) {
        logger.error("System exception", e);
        return ApiResponse.fail(500, "System busy, please try again later");
    }
}

4.3 Response Result Processing

For file download and other special responses:

@GetMapping("/export")
public ResponseEntity<Resource> exportData(@RequestParam String type) {
    String filename = "data." + type;
    Resource resource = exportService.exportData(type);
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
}

5. Logging and Performance Monitoring

5.1 Request Logging Aspect

@Aspect
@Component
@Slf4j
public class RequestLogAspect {
    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long elapsed = System.currentTimeMillis() - startTime;
        log.info("[{}] {} {} - {}ms (params: {})",
                request.getMethod(), request.getRequestURI(), request.getRemoteAddr(), elapsed,
                getParamsString(joinPoint.getArgs()));
        return result;
    }

    private String getParamsString(Object[] args) {
        return Arrays.stream(args)
                .filter(arg -> !(arg instanceof HttpServletRequest || arg instanceof HttpServletResponse))
                .map(Object::toString)
                .collect(Collectors.joining(", "));
    }
}

5.2 Slow Request Monitoring

@Aspect
@Component
@Slf4j
public class SlowRequestAspect {
    @Value("${app.slow-request-threshold:5000}")
    private long threshold;

    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object monitorSlowRequest(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long elapsed = System.currentTimeMillis() - start;
        if (elapsed > threshold) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            log.warn("Slow endpoint warning: {} took {}ms", signature.getMethod().getName(), elapsed);
        }
        return result;
    }
}

6. Security Control and Permission Management

6.1 Interface Permission Control

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
                .and()
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                .addFilter(new JwtAuthorizationFilter(authenticationManager()));
    }
}

6.2 Method‑level Permission Annotations

@RestController
@RequestMapping("/api/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public class UserAdminController {
    @DeleteMapping("/{id}")
    @PreAuthorize("hasAuthority('user:delete')")
    public ApiResponse<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ApiResponse.success();
    }
}

7. Controller Layer Testing Strategies

7.1 Unit Test Example

@WebMvcTest(UserController.class)
@AutoConfigureMockMvc(addFilters = false)
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    public void testGetUserById() throws Exception {
        User mockUser = new User(1L, "testUser", "[email protected]");
        when(userService.getUserById(1L)).thenReturn(mockUser);

        mockMvc.perform(get("/api/v1/users/1").contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data.username").value("testUser"));
    }
}

7.2 Integration Test Example

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase
public class UserControllerIntegrationTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testCreateUser() {
        UserCreateRequest request = new UserCreateRequest("newUser", "[email protected]", "password123", 25);
        ResponseEntity<ApiResponse> response = restTemplate.postForEntity("/api/v1/users", request, ApiResponse.class);
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody().getData());
    }
}

8. Advanced Features and Performance Optimization

8.1 Asynchronous Controllers

@RestController
@RequestMapping("/api/async")
public class AsyncController {
    @GetMapping("/data")
    public Callable<ApiResponse<String>> getAsyncData() {
        return () -> {
            Thread.sleep(3000); // simulate long‑running task
            return ApiResponse.success("Asynchronous processing completed");
        };
    }

    @GetMapping("/deferred")
    public DeferredResult<ApiResponse<String>> getDeferredResult() {
        DeferredResult<ApiResponse<String>> result = new DeferredResult<>(5000L);
        CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(3000);
                result.setResult(ApiResponse.success("Deferred result returned"));
            } catch (InterruptedException e) {
                result.setErrorResult(ApiResponse.fail(500, "Processing failed"));
            }
        });
        return result;
    }
}

8.2 Response Cache Control

@GetMapping("/cached")
@ResponseCache(duration = 3600) // custom annotation
public ApiResponse<List<Product>> getProducts() {
    return ApiResponse.success(productService.getAllProducts());
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseCache {
    int duration() default 60; // cache time in seconds
}

9. Summary of Controller Layer Design Principles

Single Responsibility Principle: each Controller handles a single business domain

Keep it Slim: Controllers should contain only routing and simple parameter logic

Consistent Style: uniform URL naming, parameter passing, and response format

Proper Layering: push business logic down to the Service layer

Defensive Programming: validate all input parameters

Clear Documentation: maintain API docs with tools like Swagger

Performance Awareness: consider response time and concurrency

Security First: apply appropriate security controls to all endpoints

By following these principles and practices, you can build a clear, maintainable, high‑performance, and secure Controller layer that provides a solid foundation for the entire Spring Boot application.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaException Handlingbest practicesSpring BootSecurityControllerRESTful API
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.