Week 4 Day 5 — Exceptions in REST and Spring
Goal
Today I want the HTTP and transaction rules that interviews pair with Java exceptions.
Main questions:
- What should a REST service throw?
- How does
@ControllerAdvicemap types to status codes? - What is the
@Transactionalrollback default? - How do validation failures fit?
- What do I still
newvs what Spring handles?
1. What a REST service throws
The service throws unchecked domain exceptions. The web layer maps them to HTTP. The service does not build ResponseEntity for errors.
| Failure | Typical type | HTTP |
|---|---|---|
| Missing aggregate | OrderNotFoundException | 404 |
| Bad input that passed JSON parse | IllegalArgumentException / validation | 400 |
| Bean Validation on the DTO | MethodArgumentNotValidException | 400 |
| Duplicate / conflict | DuplicateOrderException | 409 |
| Downstream timeout I cannot hide | PaymentFailedException | 502 / 503 |
| Bug | uncaught RuntimeException | 500 |
I do not throw IOException from OrderService.place. I do not return null to mean 404.
Memory sentence:
Services throw domain types. The web layer chooses the status code.
2. Mapping at the boundary
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
ProblemDetail notFound(OrderNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(DuplicateOrderException.class)
@ResponseStatus(HttpStatus.CONFLICT)
ProblemDetail conflict(DuplicateOrderException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
}
}
@ControllerAdvice is that boundary from Day 4. One place, one log if needed, stable JSON (ProblemDetail is the Spring Boot 3 default shape, RFC 9457).
@ResponseStatus(HttpStatus.NOT_FOUND) on the exception class itself is fine for tiny apps. Advice is better when the body must include an id or a list of field errors.
Controllers stay thin:
@GetMapping("/{id}")
OrderResponse get(@PathVariable long id) {
return OrderResponse.from(orders.get(id)); // throws OrderNotFoundException
}
No try/catch in the controller unless this one endpoint compensates in a unique way.
3. @Transactional and rollback
Default Spring rule:
A
@Transactionalmethod rolls back on unchecked exceptions (RuntimeExceptionandError). It commits on a checkedExceptionunless I configure otherwise.
@Transactional
public void place(CreateOrderRequest request) throws InvoiceException {
orders.save(...);
invoices.issue(...); // throws checked InvoiceException
}
If issue throws a checked InvoiceException, the transaction commits by default. The order is saved. That surprise is a classic interview trap.
Fixes:
- throw unchecked domain exceptions from the service
- or
@Transactional(rollbackFor = Exception.class)when I must keep a checked type
Self-invocation still skips the proxy (Week 3). A this.place() call also skips the rollback interceptor.
Memory sentence:
Default rollback is for runtime exceptions. Checked exceptions commit unless I set
rollbackFor.
4. Validation vs domain
Bean Validation (@NotBlank, @Min) on a request record fails before the controller method, as MethodArgumentNotValidException (or ConstraintViolationException on @Validated params). That is not a domain exception. Advice maps it to 400 with field names.
Domain rules that need the database (“sku exists”, “quantity within stock”) throw from the service after a lookup. Do not pretend @Min(1) is the whole business.
5. What Spring already did
- Servlet container: uncaught exceptions become 500 unless I handle them.
DataAccessException: JDBC/JPA translated, unchecked, rollback-friendly.RestClient/WebClient: HTTP errors as exceptions I translate in a client adapter.- I still use try-with-resources for files I open in an import job.
I do not close EntityManager in these handlers.
6. Common traps
Trap 1: catch (Exception e) { return ResponseEntity.ok(null); } in a controller.
Trap 2: Assuming a checked exception from a transactional method undoes the insert.
Trap 3: @ResponseStatus on a type and a conflicting @ExceptionHandler I forgot about.
Trap 4: Mapping every RuntimeException to 400. Bugs become client errors. Unknown runtime → 500.
Trap 5: Putting @Transactional on the controller so exceptions “just work.” Transactions belong on the use case (service), with rollback rules I can test.
Practice Questions and Answers
Question 1
What should a REST service throw?
Answer:
Unchecked domain exceptions that name the business failure. @ControllerAdvice maps them to HTTP status and a body. The service does not catch-and-return status codes.
Question 2
Does @Transactional roll back on Exception?
Answer:
Not by default. It rolls back on RuntimeException and Error. A checked Exception commits unless I set rollbackFor. That is why domain failures should be unchecked.
Question 3
Why is @ControllerAdvice better than try/catch in every controller method?
Answer:
One mapping, one JSON shape, one place to log. Controllers stay as the HTTP adapter. New endpoints inherit the policy.
Question 4
How do I handle a missing order vs a missing JSON field?
Answer:
Missing JSON / validation: MethodArgumentNotValidException → 400. Missing order after a lookup: OrderNotFoundException → 404. They are different types on purpose.
Question 5
Why does Spring’s DataAccessException hierarchy exist?
Answer:
To translate vendor-specific checked SQLException into an unchecked, portable tree (DuplicateKeyException, …) so services stay readable, transactions roll back, and I can map persistence failures without importing JDBC into the web layer.
Memory sentences
Services throw domain types. The web layer chooses the status code.
Default rollback is unchecked. Checked commits unless
rollbackFor.
Unknown
RuntimeExceptionis a 500, not a 400.
Next: Week 4 Review