Improving Controller Layer Logic, Unified Response Structure, and Parameter Validation in Spring Boot
This article explains how to design a clean Controller layer in Spring Boot, introduces a unified response format with Result and ResponseBodyAdvice, demonstrates parameter validation using JSR‑303 and Spring Validation, and shows custom validation annotations and global exception handling to keep controller code concise and maintainable.
Hello, I am a top architect.
An Excellent Controller Layer Logic
The Controller provides data interfaces and acts as an indispensable supporting role in both traditional three‑tier and modern COLA architectures. It mainly receives requests, delegates business execution to services, handles exceptions, and returns responses.
Current Problems
Parameter validation is tightly coupled with business code, violating the Single Responsibility Principle.
Identical exceptions are thrown in many places, causing code duplication.
Response formats for success and error are inconsistent, making API integration unfriendly.
Refactoring the Controller Layer
Unified Return Structure
Using a unified return type makes it clear whether an API call succeeded, regardless of front‑end/back‑end separation.
// Define return data structure
public interface IResult {
Integer getCode();
String getMessage();
}
// Common result enum
public enum ResultEnum implements IResult {
SUCCESS(2001, "Interface call succeeded"),
VALIDATE_FAILED(2002, "Parameter validation failed"),
COMMON_FAILED(2003, "Interface call failed"),
FORBIDDEN(2004, "No permission to access resource");
// ... getters and constructors omitted
}
// Unified response wrapper
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result
{
private Integer code;
private String message;
private T data;
// static factory methods for success/failure omitted for brevity
}After defining the wrapper, each controller can simply return Result.success(data) . To avoid repetitive wrapping, we use Spring's ResponseBodyAdvice :
public interface ResponseBodyAdvice
{
boolean supports(MethodParameter returnType, Class
> converterType);
@Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class
> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice
{
@Override
public boolean supports(MethodParameter returnType, Class
> converterType) {
return true; // apply to all responses
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class
> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}Parameter Validation
JSR‑303 (validation‑api) and its popular implementation Hibernate Validator provide a standard way to validate parameters. Spring Validation wraps these rules for @RequestParam , @PathVariable , and @RequestBody .
1️⃣ @PathVariable and @RequestParam Validation
@RestController
@RequestMapping("/pretty")
public class TestController {
@GetMapping("/{num}")
public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {
return num * num;
}
@GetMapping("/getByEmail")
public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {
TestDTO dto = new TestDTO();
dto.setEmail(email);
return dto;
}
}If validation fails, Spring throws MethodArgumentNotValidException (for body) or ConstraintViolationException (for path/query parameters).
2️⃣ @RequestBody Validation
// DTO
@Data
public class TestDTO {
@NotBlank
private String userName;
@NotBlank @Length(min = 6, max = 20)
private String password;
@NotNull @Email
private String email;
}
// Controller
@RestController
@RequestMapping("/pretty")
public class TestController {
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO testDTO) {
testService.save(testDTO);
}
}3️⃣ Custom Validation Rules
// Custom annotation
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
boolean required() default true;
String message() default "Invalid mobile number format";
Class
[] groups() default {};
Class
[] payload() default {};
}
// Validator implementation
public class MobileValidator implements ConstraintValidator
{
private boolean required;
private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
@Override
public void initialize(Mobile annotation) { this.required = annotation.required(); }
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext ctx) {
if (required) return pattern.matcher(value).matches();
if (StringUtils.hasText(value)) return pattern.matcher(value).matches();
return true;
}
}Custom Exceptions and Global Exception Handling
// Custom exceptions
public class ForbiddenException extends RuntimeException { public ForbiddenException(String msg) { super(msg); } }
public class BusinessException extends RuntimeException { public BusinessException(String msg) { super(msg); } }
// Global handler
@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {
@ExceptionHandler(BusinessException.class)
public Result
handleBusinessException(BusinessException ex) { return Result.failed(ex.getMessage()); }
@ExceptionHandler(ForbiddenException.class)
public Result
handleForbiddenException(ForbiddenException ex) { return Result.failed(ResultEnum.FORBIDDEN); }
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result
handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
StringBuilder sb = new StringBuilder("Validation failed: ");
for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
sb.append(fe.getField()).append(": ").append(fe.getDefaultMessage()).append(", ");
}
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), sb.toString());
}
@ExceptionHandler(ConstraintViolationException.class)
public Result
handleConstraintViolationException(ConstraintViolationException ex) {
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
}
@ExceptionHandler(Exception.class)
public Result
handle(Exception ex) { return Result.failed(ex.getMessage()); }
}Conclusion
After applying these changes, controller code becomes concise, each parameter’s validation rules are explicit, return data is uniform, and exceptions are centrally handled, allowing developers to focus on business logic while keeping the codebase clean and maintainable.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.