β¬ οΈ Back to Validation Index
Exception Handling
Overview
Exception handling is the final validation layer, converting validation failures into standardized HTTP responses. Smart Supply Pro uses two exception handlers:
- GlobalExceptionHandler - Framework-level (JSR-380, HTTP errors, authentication)
- BusinessExceptionHandler - Domain-level (business rule violations)
This layered approach ensures validation failures at every level map to appropriate HTTP status codes and error responses.
Validation Exception Flow
(POST/PUT)"] B["Controller
@Validated"] C["JSR-380 Constraints
@NotNull, @NotBlank"] D["GlobalExceptionHandler"] E["Service Layer
Custom Validators"] F["Business Rules
Uniqueness, Safety"] G["BusinessExceptionHandler"] H["Security Layer
InventoryItemSecurityValidator"] I["Field Authorization
Role-based Access"] J["Success: 200 OK
Created: 201
Updated: 204"] K["Error Response
400/404/409/422/401/403"] A -->|Request body| B B -->|Bind & validate| C C -->|Constraint
violated| D D -->|MethodArgumentNotValidException
ConstraintViolationException| K C -->|Valid| E E -->|Invoke
validators| F F -->|DuplicateResource
InvalidRequest
IllegalArgument| G G -->|Convert to
HTTP response| K F -->|Valid| H H -->|Check field
permissions| I I -->|Unauthorized
access| K I -->|Authorized| J style A fill:#e1f5ff style J fill:#c8e6c9 style K fill:#ffcdd2 style D fill:#fff9c4 style G fill:#fff9c4
Exception Hierarchy
Exception
βββ RuntimeException
β βββ InvalidRequestException β 400 (custom validation)
β βββ DuplicateResourceException β 409 (business conflict)
β βββ IllegalStateException β 409 (state conflict)
β
βββ Spring Framework
βββ MethodArgumentNotValidException β 400 (JSR-380)
βββ ConstraintViolationException β 400 (JSR-380)
βββ HttpMessageNotReadableException β 400 (JSON parsing)
βββ ResponseStatusException β 404/422/500 (explicit)
βββ AuthenticationException β 401 (authentication)
βββ AccessDeniedException β 403 (authorization)
BusinessExceptionHandler
Location
src/main/java/com/smartsupplypro/inventory/exception/BusinessExceptionHandler.java
Responsibilities
| Exception | Status | Handler | Scenario |
|---|---|---|---|
| InvalidRequestException | 400 | handleInvalidRequest() |
Custom validation failure |
| DuplicateResourceException | 409 | handleDuplicateResource() |
Uniqueness constraint violation |
| IllegalStateException | 409 | handleBusinessStateConflict() |
Invalid state transition |
Implementation
/**
* Business exception handler for domain-specific application logic failures.
* Complements GlobalExceptionHandler by focusing on Smart Supply Pro business rules.
*/
@Order(Ordered.HIGHEST_PRECEDENCE) // Runs BEFORE GlobalExceptionHandler
@RestControllerAdvice
public class BusinessExceptionHandler {
/**
* Handles custom validation failures with field-level details.
* Example: Quantity validation, price validation, format validation
*/
@ExceptionHandler(InvalidRequestException.class)
public ResponseEntity<ErrorResponse> handleInvalidRequest(InvalidRequestException ex) {
String message = ex.hasFieldErrors()
? "Validation failed: " + ex.getFieldErrors().size() + " field error(s)"
: (ex.getMessage() != null ? ex.getMessage() : "Invalid request");
return ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST)
.message(message)
.build();
}
/**
* Handles uniqueness constraint violations.
* Example: Duplicate supplier name, duplicate item (name + price)
*/
@ExceptionHandler(DuplicateResourceException.class)
public ResponseEntity<ErrorResponse> handleDuplicateResource(DuplicateResourceException ex) {
String message = ex.hasDetailedContext()
? ex.getClientMessage()
: (ex.getMessage() != null ? ex.getMessage() : "Duplicate resource");
return ErrorResponse.builder()
.status(HttpStatus.CONFLICT)
.message(message)
.build();
}
/**
* Handles business state violations.
* Example: Cannot delete supplier with linked items
*/
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<ErrorResponse> handleBusinessStateConflict(IllegalStateException ex) {
String message = (ex.getMessage() != null && !ex.getMessage().isBlank())
? ex.getMessage()
: "Business rule conflict";
return ErrorResponse.builder()
.status(HttpStatus.CONFLICT)
.message(message)
.build();
}
}GlobalExceptionHandler
Location
src/main/java/com/smartsupplypro/inventory/exception/GlobalExceptionHandler.java
Scope
| Category | Status | Handler | Handles |
|---|---|---|---|
| Validation | 400 | handleValidation() |
MethodArgumentNotValidException (@Valid) |
| Constraints | 400 | handleConstraint() |
ConstraintViolationException (JSR-380) |
| Parsing | 400 | handleParsingError() |
HttpMessageNotReadableException (JSON) |
| Parameters | 400 | handleParameterError() |
Missing/type-mismatch parameters |
| Authentication | 401 | handleAuthentication() |
AuthenticationException |
| Authorization | 403 | handleAuthorization() |
AccessDeniedException |
| Not Found | 404 | handleNotFound() |
NoSuchElementException |
| Conflicts | 409 | handleDataIntegrity() |
DataIntegrityViolationException |
| Concurrency | 409 | handleOptimisticLock() |
ObjectOptimisticLockingFailureException |
| Pass-Through | 404/422/500 | handleResponseStatus() |
ResponseStatusException |
| Fallback | 500 | handleUnexpected() |
Any uncaught Exception |
Key Features
@Order(Ordered.HIGHEST_PRECEDENCE + 1) // Runs AFTER BusinessExceptionHandler as catch-all
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1. JSR-380 VALIDATION FAILURES
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
// Extracts first @Valid error for client feedback
String message = ex.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(fe -> fe.getField() + " " + fe.getDefaultMessage())
.orElse("Validation failed");
return ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST)
.message(sanitize(message))
.build();
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraint(ConstraintViolationException ex) {
// Extracts JSR-380 constraint violation (@NotNull, @NotBlank, etc.)
String message = ex.getConstraintViolations().stream()
.findFirst()
.map(v -> v.getPropertyPath() + " " + v.getMessage())
.orElse("Constraint violation");
return ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST)
.message(sanitize(message))
.build();
}
// 2. SECURITY EXCEPTIONS
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthentication(AuthenticationException ex) {
// Generic message prevents user enumeration attacks
return ErrorResponse.builder()
.status(HttpStatus.UNAUTHORIZED)
.message("Authentication required")
.build();
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAuthorization(AccessDeniedException ex) {
// Generic message prevents unauthorized access hints
return ErrorResponse.builder()
.status(HttpStatus.FORBIDDEN)
.message("Access denied")
.build();
}
// 3. SAFETY NET - Sanitizes error messages to prevent information disclosure
private String sanitize(String message) {
if (message == null) return "Unknown error";
return message
.replaceAll("\\b[A-Za-z]:\\\\[\\w\\\\.-]+", "[PATH]") // Windows paths
.replaceAll("/[\\w/.-]+\\.(java|class)", "[INTERNAL]") // Unix paths
.replaceAll("\\bcom\\.smartsupplypro\\.[\\w.]+", "[INTERNAL]") // Package names
.replaceAll("(?i)\\bSQL.*", "Database operation failed") // SQL fragments
.replaceAll("(?i)\\bPassword.*", "Authentication failed") // Credentials
.replaceAll("(?i)\\bToken.*", "Authentication failed") // Tokens
.trim();
}
}Custom Exceptions
InvalidRequestException
/**
* Enterprise exception for validation failures.
* Supports field-level errors and severity classification.
*/
public class InvalidRequestException extends RuntimeException {
private final ValidationSeverity severity;
private final String validationCode;
private final Map<String, String> fieldErrors;
private final List<String> generalErrors;
// Constructors
public InvalidRequestException(String message) { }
public InvalidRequestException(String message, ValidationSeverity severity,
String validationCode) { }
public InvalidRequestException(String message, Map<String, String> fieldErrors) { }
// Accessors
public ValidationSeverity getSeverity() { }
public String getValidationCode() { }
public Map<String, String> getFieldErrors() { }
}
// Severity levels
public enum ValidationSeverity {
LOW, // Informational - doesn't block operation
MEDIUM, // Standard validation failure - operation blocked
HIGH // Critical - security or integrity concern
}DuplicateResourceException
/**
* Enterprise exception for uniqueness constraint violations.
* Provides detailed conflict context for client error handling.
*/
public class DuplicateResourceException extends RuntimeException {
private final String conflictField;
private final String existingResourceId;
private final Map<String, String> conflictDetails;
public DuplicateResourceException(String message) { }
public DuplicateResourceException(String message, String conflictField,
String existingResourceId) { }
// Accessors for conflict resolution
public String getConflictField() { }
public String getExistingResourceId() { }
}HTTP Status Mapping
Status Code by Exception Type
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β STATUS CODE β EXCEPTION CLASS β USE CASE β
ββββββββββββββββΌβββββββββββββββββββββββββββββββΌββββββββββββββββββββββ€
β 400 β MethodArgumentNotValidException β @Valid fails β
β 400 β ConstraintViolationException β JSR-380 fails β
β 400 β HttpMessageNotReadableException β Bad JSON β
β 400 β InvalidRequestException β Custom validation fail β
β 400 β IllegalArgumentException β Invalid argument β
ββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββ€
β 401 β AuthenticationException β No credentials β
ββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββ€
β 403 β AccessDeniedException β User lacks role β
ββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββ€
β 404 β NoSuchElementException β Resource not foundβ
β 404 β ResponseStatusException (404) β Custom 404 β
ββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββ€
β 409 β DuplicateResourceException β Duplicate entity β
β 409 β DataIntegrityViolationException β DB constraint β
β 409 β ObjectOptimisticLockingFailure β Concurrent update β
β 409 β IllegalStateException β Invalid state β
ββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββ€
β 422 β ResponseStatusException (422) β Unprocessable β
ββββββββββββββββΌβββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββ€
β 500 β Exception (any other) β Server error β
ββββββββββββββββ΄βββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββ
Error Response Format
Response Structure
{
"status": "BAD_REQUEST",
"statusCode": 400,
"message": "Validation failed: name cannot be empty",
"timestamp": "2024-01-15T10:30:45.123Z",
"path": "/api/inventory/items"
}Example Error Scenarios
Scenario 1: JSR-380 Constraint Violation (400)
Request:
POST /api/inventory/items
{
"name": "",
"quantity": 100,
"price": 25.50,
"supplierId": "SUPP-001"
}Response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"status": "BAD_REQUEST",
"statusCode": 400,
"message": "name cannot be empty",
"timestamp": "2024-01-15T10:30:45.123Z",
"path": "/api/inventory/items"
}Handler:
GlobalExceptionHandler.handleValidation() (catches
MethodArgumentNotValidException from @NotBlank)
Scenario 2: Custom Validation Failure (400)
Request:
PUT /api/inventory/items/ITEM-123
{
"name": "Widget A",
"quantity": 100,
"price": 25.50,
"supplierId": "SUPP-001"
}Validation Chain: 1. β JSR-380 constraints pass 2. β InventoryItemValidator.validateBase() passes 3. β InventoryItemValidator.validateInventoryItemNotExists() fails (duplicate found)
Response:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"status": "CONFLICT",
"statusCode": 409,
"message": "Another inventory item with this name and price already exists",
"timestamp": "2024-01-15T10:30:45.123Z",
"path": "/api/inventory/items/ITEM-123"
}Handler:
BusinessExceptionHandler.handleDuplicateResource()
(catches DuplicateResourceException)
Scenario 3: Negative Stock Adjustment (422)
Request:
POST /api/inventory/items/ITEM-123/adjust
{
"delta": -150
}Current quantity: 100
Validation Chain: 1. β JSR-380 constraints pass 2. β Item exists check passes 3. β assertFinalQuantityNonNegative(100 + (-150) = -50) fails
Response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"status": "UNPROCESSABLE_ENTITY",
"statusCode": 422,
"message": "Resulting stock cannot be negative",
"timestamp": "2024-01-15T10:30:45.123Z",
"path": "/api/inventory/items/ITEM-123/adjust"
}Handler:
GlobalExceptionHandler.handleResponseStatus()
(catches ResponseStatusException with 422 status)
Scenario 4: Unauthorized Field Access (403)
Request (USER role, attempting to update price):
PUT /api/inventory/items/ITEM-123
{
"name": "Widget A",
"quantity": 150,
"price": 99.99, β USER cannot modify price
"supplierId": "SUPP-001"
}Validation Chain: 1. β JSR-380 constraints pass 2. β Custom validators pass 3. β InventoryItemSecurityValidator.validateUpdatePermissions() fails (role insufficient)
Response:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"status": "FORBIDDEN",
"statusCode": 403,
"message": "Access denied",
"timestamp": "2024-01-15T10:30:45.123Z",
"path": "/api/inventory/items/ITEM-123"
}Handler:
GlobalExceptionHandler.handleAuthorization()
(catches AccessDeniedException)
Best Practices
1. Exception Specificity
// β
Good: Specific exception for business context
throw new DuplicateResourceException("Supplier already exists");
throw new InvalidRequestException("Quantity must be non-negative");
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Item not found");
// β Avoid: Generic exceptions
throw new RuntimeException("Error");
throw new Exception("Something failed");2. Handler Ordering (Precedence)
Order 1 (HIGHEST_PRECEDENCE) β BusinessExceptionHandler
Order 2 (HIGHEST_PRECEDENCE + 1) β GlobalExceptionHandler (catch-all)
Order 3 (default) β Spring framework handlers
This ensures:
- Domain exceptions caught first
- Framework exceptions caught second
- Prevents masking business logic with framework details
3. Message Sanitization (Security)
// β
Good: Sanitized messages prevent information disclosure
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Item not found");
// β Avoid: Exposing internal details
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
"Item not found in inventory_items table on sspdb database");
// GlobalExceptionHandler automatically removes:
// - File paths: C:\Users\... β [PATH]
// - Class names: com.smartsupplypro.* β [INTERNAL]
// - SQL fragments: SQL syntax error β Database operation failed
// - Credentials: password, token fields β Authentication failed4. Logging with Correlation
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex) {
String correlationId = MDC.get("correlationId"); // From request context
logger.error("Unexpected error [{}]", correlationId, ex);
return ErrorResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.message("Unexpected server error")
.build();
}Related Documentation
- Validation Index - Multi-layer validation framework overview
- JSR-380 Constraints - Declarative field validation
- Custom Validators - Domain-specific validation logic
- Validation Patterns - Best practices