Spring Boot Exception Handling and Validation Patterns
Avoiding Multiple Exception Handlers
Section titled “Avoiding Multiple Exception Handlers”As an application grows, it’s easy to accumulate many narrow @ExceptionHandler methods. Three ways to keep this manageable:
Option 1: Common Base Exception
Section titled “Option 1: Common Base Exception”public class ApplicationException extends RuntimeException {}@ExceptionHandler(ApplicationException.class)public ResponseEntity<?> handleAppException(ApplicationException ex) { return ResponseEntity.badRequest().body(ex.getMessage());}Option 2: Handle Multiple Exceptions
Section titled “Option 2: Handle Multiple Exceptions”@ExceptionHandler({ IllegalArgumentException.class, IllegalStateException.class})Option 3: Generic Fallback
Section titled “Option 3: Generic Fallback”@ExceptionHandler(Exception.class)Recommended hierarchy: business exceptions, then validation exceptions, then a generic fallback last.
Request Sanitization
Section titled “Request Sanitization”Bean Validation checks structure and format, but sanitization is a related, distinct concern:
- Bean Validation
- Input trimming
- Escape HTML
- Regex validation
- OWASP libraries for XSS protection
@Pattern(regexp = "^[a-zA-Z ]+$")private String name;Returning Validation Errors
Section titled “Returning Validation Errors”Handle MethodArgumentNotValidException globally to return field-level error messages instead of a generic 400.
@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<Map<String, String>> handleValidationErrors( MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()) );
return ResponseEntity.badRequest().body(errors);}Example response:
{ "name": "Name cannot be blank", "email": "Invalid email"}Custom Bean Validation
Section titled “Custom Bean Validation”Step 1: Custom Annotation
Section titled “Step 1: Custom Annotation”@Target({FIELD})@Retention(RUNTIME)@Constraint(validatedBy = PhoneValidator.class)public @interface ValidPhone {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};}Step 2: Validator
Section titled “Step 2: Validator”public class PhoneValidator implements ConstraintValidator<ValidPhone, String> {
public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && value.matches("\\d{10}"); }}Step 3: Use the Annotation
Section titled “Step 3: Use the Annotation”@ValidPhoneprivate String phoneNumber;Key Takeaways
Section titled “Key Takeaways”- Group exception handlers around a common base exception (or list multiple exception types in one
@ExceptionHandler) rather than letting narrow handlers proliferate; keep a genericExceptionfallback last. - Sanitization (trimming, escaping, regex, XSS protection) is a separate concern from Bean Validation — validation checks shape, sanitization checks safety.
MethodArgumentNotValidExceptiongives youBindingResult.getFieldErrors()to return field-level messages, which is far more useful to API clients than a generic 400.- A custom Bean Validation annotation needs three pieces: the annotation itself (
@Constraint(validatedBy = ...)), aConstraintValidatorimplementation, and applying the annotation to the field.