Exception Translation
Pattern Overview
Services throw domain-specific exceptions. The global exception handler catches these and translates to appropriate HTTP responses.
Service → Domain Exception Pattern
Services throw domain exceptions based on business rule violations:
@Service
@RequiredArgsConstructor
public class SupplierServiceImpl implements SupplierService {
private final SupplierRepository repository;
private final SupplierValidator validator;
public SupplierDTO create(CreateSupplierDTO dto) {
// Throws IllegalStateException (domain logic failure)
if (repository.existsByNameIgnoreCase(dto.getName())) {
throw new IllegalStateException("Supplier with name already exists");
}
return mapper.toDTO(repository.save(mapper.toEntity(dto)));
}
}GlobalExceptionHandler → HTTP Response
The global exception handler catches exceptions and translates to HTTP:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Handle duplicate supplier
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<ErrorResponse> handleIllegalState(
IllegalStateException ex) {
log.warn("Business rule violation: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResponse.builder()
.code("CONFLICT")
.message(ex.getMessage())
.timestamp(LocalDateTime.now())
.build());
}
// Handle missing supplier
@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
NoSuchElementException ex) {
log.warn("Resource not found: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.builder()
.code("NOT_FOUND")
.message(ex.getMessage())
.timestamp(LocalDateTime.now())
.build());
}
// Handle validation failures
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgument(
IllegalArgumentException ex) {
log.warn("Invalid argument: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponse.builder()
.code("BAD_REQUEST")
.message(ex.getMessage())
.timestamp(LocalDateTime.now())
.build());
}
}Exception Mapping Guide
| Domain Exception | HTTP Status | Use Case |
|---|---|---|
IllegalStateException |
409 Conflict | Business rule violation, duplicate resource |
NoSuchElementException |
404 Not Found | Resource not found |
IllegalArgumentException |
400 Bad Request | Invalid input parameters |
UnsupportedOperationException |
501 Not Implemented | Operation not available |
Example: Complete Flow
1. Controller Call
@RestController
@RequestMapping("/api/suppliers")
public class SupplierController {
@PostMapping
public ResponseEntity<SupplierDTO> create(
@RequestBody CreateSupplierDTO dto) {
// Call service (may throw exception)
return ResponseEntity.ok(service.create(dto));
}
}2. Service Throws Exception
@Service
public class SupplierServiceImpl implements SupplierService {
public SupplierDTO create(CreateSupplierDTO dto) {
if (repository.existsByNameIgnoreCase(dto.getName())) {
throw new IllegalStateException("Duplicate supplier");
}
return mapper.toDTO(repository.save(mapper.toEntity(dto)));
}
}3. Handler Catches & Translates
Controller throws → GlobalExceptionHandler catches
→ Exception matched to @ExceptionHandler
→ Returns ErrorResponse with HTTP 409
4. Client Receives
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"code": "CONFLICT",
"message": "Supplier with name already exists",
"timestamp": "2025-01-15T10:30:00"
}Anti-Pattern: Catching and Swallowing
// ❌ Bad - Exception caught but not handled
public SupplierDTO create(CreateSupplierDTO dto) {
try {
return mapper.toDTO(repository.save(mapper.toEntity(dto)));
} catch (Exception e) {
return null; // Lost error information
}
}Best Practice: Let Exceptions Propagate
// ✅ Good - Let exceptions propagate to handler
public SupplierDTO create(CreateSupplierDTO dto) {
if (repository.existsByNameIgnoreCase(dto.getName())) {
throw new IllegalStateException("Duplicate supplier");
}
return mapper.toDTO(repository.save(mapper.toEntity(dto)));
}