At scale, how would you design a consistent HTTP error-mapping strategy across a Spring application, and where do @ResponseStatus, ResponseStatusException, and ProblemDetail fit?
answer
- One source of truth: @ControllerAdvice / ResponseEntityExceptionHandler
- ProblemDetail (RFC 7807) as the uniform body
- Keep domain exceptions HTTP-agnostic
- ResponseStatusException implements ErrorResponse in Spring 6
- Watch: leak, include-message, resolver precedence, class explosion
basics
~20 sCentralize error mapping in a @ControllerAdvice (often extending ResponseEntityExceptionHandler) so every error returns one consistent body, ideally a ProblemDetail (RFC 7807). Use @ResponseStatus/ResponseStatusException for simple local cases, but don't scatter the contract across many exceptions.
solid answer
~40 sFor a maintainable API I want one authoritative place defining the error contract, not status codes scattered across dozens of annotated exceptions. I keep domain exceptions transport-agnostic (no @ResponseStatus in the domain layer) and translate them in a global @ControllerAdvice — in Spring 6 typically extending ResponseEntityExceptionHandler, returning ProblemDetail (RFC 7807) so bodies carry type/title/status/detail/instance and custom properties consistently. @ResponseStatus stays useful for trivial, stable cases and for non-200 success codes on methods; ResponseStatusException is handy for inline, dynamic errors, and in Spring 6 it already implements ErrorResponse so it renders a ProblemDetail. I'm careful about the resolver chain (ExceptionHandler wins), the reason/sendError body-suppression gotcha (include-message=never), never leaking internal messages, and keeping error semantics uniform across modules. I also make errors testable and documented (OpenAPI) so clients can rely on them.
code
java · 18 lines@RestControllerAdvice
class GlobalErrors extends ResponseEntityExceptionHandler {
@ExceptionHandler(OrderNotFound.class)
ProblemDetail handleNotFound(OrderNotFound ex) {
ProblemDetail pd = ProblemDetail
.forStatusAndDetail(HttpStatus.NOT_FOUND, "Order not found");
pd.setType(URI.create("https://api.example.com/errors/order-not-found"));
pd.setProperty("orderId", ex.getId()); // extension member
return pd; // Spring renders application/problem+json
}
@ExceptionHandler(InsufficientFunds.class)
ProblemDetail handleFunds(InsufficientFunds ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, "Insufficient funds");
}
}
// Domain layer stays clean: class OrderNotFound extends RuntimeException { ... } (no @ResponseStatus)go deeper
Aware a global handler can standardize errors.
Can build a @ControllerAdvice with @ExceptionHandler methods.
Uses ProblemDetail and understands resolver precedence and body suppression.
Designs a layered, leak-safe, documented, cross-module error contract and places each tool deliberately.
## The problem at scale Small apps can sprinkle `@ResponseStatus` on exceptions. As an API grows, that spreads the HTTP contract across many classes, couples the domain layer to web concerns, and yields inconsistent bodies (some via `sendError`/`/error`, some via ad-hoc handlers). A principal-level answer imposes a single, coherent error model. ## A layered strategy **1. Keep the domain transport-agnostic.** Domain/service exceptions (e.g. `OrderNotFound`, `InsufficientFunds`) carry business meaning and should NOT be annotated with `@ResponseStatus` — that would leak HTTP into the domain. They stay plain exceptions. **2. Translate at the edge with a global advice.** A `@ControllerAdvice` centralizes mapping. In Spring 6, extend `ResponseEntityExceptionHandler` (which already maps built-in Spring exceptions to `ProblemDetail`) and add `@ExceptionHandler` methods for your domain types. This is the single source of truth for status + body. **3. Standardize the body with ProblemDetail (RFC 7807).** `org.springframework.http.ProblemDetail` models `type`, `title`, `status`, `detail`, `instance`, plus extension properties. Using it everywhere gives clients a predictable, spec-compliant shape: ```java @ExceptionHandler(OrderNotFound.class) ProblemDetail handle(OrderNotFound ex) { ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); pd.setType(URI.create("https://api.example.com/errors/order-not-found")); pd.setProperty("orderId", ex.getId()); return pd; } ``` ## Where the three tools fit - **`@ResponseStatus` on exceptions:** fine for tiny apps or a couple of stable, obvious mappings; also idiomatic for non-200 success on methods (201/204). Avoid as the primary API-wide error mechanism because of body suppression (`include-message=never`), no headers, no structured body, and contract scatter. - **`ResponseStatusException`:** great for inline, dynamic, one-off errors and adding headers; in Spring 6 it implements `ErrorResponse`, so `ResponseEntityExceptionHandler` renders it as a `ProblemDetail` automatically. Good glue, but still avoid making it the whole strategy — centralize. - **`ProblemDetail` + `@ControllerAdvice`/`ResponseEntityExceptionHandler`:** the backbone for a consistent, documented contract. ## Cross-cutting concerns a principal watches - **Resolver precedence:** `@ExceptionHandler` beats `@ResponseStatus`; know it so mappings don't silently conflict. - **Security / info leak:** never echo internal exception messages or stack traces; keep `include-stacktrace=never`, sanitize `detail`. - **Consistency across modules:** in a modular monolith (like Spring Modulith) decide whether each module maps its own errors or a shared web layer does; avoid divergent bodies. - **i18n:** use MessageSource (reason codes or resolving `detail`) if localization is needed. - **Testing & docs:** assert status + body in tests; publish error schemas via OpenAPI so clients can code against them. - **Observability:** log at the boundary with correlation IDs; map to metrics. ## Anti-patterns to call out - One exception class per status just to use `@ResponseStatus` (class explosion). - Mixing `sendError`-based bodies and custom JSON bodies so clients see two shapes. - Relying on `reason` for the client message while `include-message=never` hides it. - Annotating domain exceptions with HTTP concerns, coupling layers. ## Summary Centralize the contract (advice + ProblemDetail), keep the domain clean, and use `@ResponseStatus`/`ResponseStatusException` as convenient local tools that feed into — not replace — that central model.
- In Spring 6, what happens to a thrown ResponseStatusException when a ResponseEntityExceptionHandler is present?ResponseStatusException implements ErrorResponse, so ResponseEntityExceptionHandler renders it as a ProblemDetail (application/problem+json) with the status and reason mapped into the problem body, giving a consistent contract.
- Why avoid @ResponseStatus on domain-layer exceptions?It couples business exceptions to HTTP semantics, leaking a transport concern into the domain. Keeping them plain lets the same exceptions be reused (e.g. in messaging or batch) and centralizes HTTP mapping at the web edge.
- How do you prevent leaking internal details in error bodies?Set include-stacktrace/include-message conservatively, never echo raw exception messages into ProblemDetail.detail for unexpected errors, map unknown exceptions to a generic 500 body, and sanitize/allowlist what goes to clients.
saying these in an interview costs you the question
- Proposing one @ResponseStatus exception class per status as the main strategy
- Annotating domain exceptions with HTTP status codes
- Echoing raw exception messages/stack traces to clients
- Assuming ProblemDetail is automatic without any handler in older Spring versions