Interview Prep Zoneby CuriouserLabs

Question 2 of 5 · senior level

Design the error-handling contract for a REST API: @ControllerAdvice, ProblemDetail and what clients should see.

🎤 Say this first

Centralize in one @RestControllerAdvice: map the domain exception hierarchy to HTTP semantics — validation → 400/422, not-found → 404, conflict/optimistic-lock → 409, auth → 401/403, downstream failure → 502/503 with Retry-After, unknown → 500 with internals hidden. Use ProblemDetail (RFC 9457, native in Spring 6) as the wire format: machine-readable type, human title/detail, plus correlationId. Log each failure once, at the edge, with full context; clients get stable error types they can program against, never stack traces.

The full picture

A contract, not just a handler
@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    ProblemDetail invalid(MethodArgumentNotValidException e) {
        var p = ProblemDetail.forStatus(422);
        p.setType(URI.create("https://api.acme.com/errors/validation"));
        p.setProperty("violations", e.getFieldErrors().stream()
            .map(f -> Map.of("field", f.getField(), "message", f.getDefaultMessage()))
            .toList());
        return p;
    }

    @ExceptionHandler(OptimisticLockingFailureException.class)
    ProblemDetail conflict(OptimisticLockingFailureException e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT,
            "Resource was modified concurrently; refetch and retry.");
    }

    @ExceptionHandler(Exception.class)
    ProblemDetail unknown(Exception e, HttpServletRequest req) {
        log.error("unhandled [{}]", MDC.get("cid"), e);      // logged ONCE, full stack
        var p = ProblemDetail.forStatus(500);
        p.setProperty("correlationId", MDC.get("cid"));      // client can quote it
        return p;                                            // no internals leaked
    }
}
  • Resolution order matters: more specific @ExceptionHandler wins; extending ResponseEntityExceptionHandler gives you Spring's own exceptions (unreadable body, method-not-allowed) mapped consistently too.
  • Two error surfaces exist in Boot: MVC's advice chain and the container-level /error (BasicErrorController) for failures outside MVC (filter exceptions!) — align both or clients see two formats.
  • Security nuance: 404-vs-403 leaks existence of resources; decide the policy explicitly per resource type.
  • Version the error type URIs like API contracts — clients build logic on them.

🔄 Likely follow-up questions

  • 422 vs 400 for validation — what's your policy and why?
  • How do you keep error responses consistent between MVC exceptions and security filter failures?
  • How would you evolve an error format without breaking clients?