Maintaining robust API stability requires distinguishing between infrastructure faults (e.g., database connectivity) and logical violations defined by domain rules. Relying on generic RuntimeException for business failures makes it difficult to provide context-specific responses. A dedicated exception class encapsulates error codes and user-facing messages, enabling standardized error handling across the application stack.
Core Exception Model
The custom exception should extend RuntimeException to ensure it bypasses checked exception requirements while carrying rich payload data. It serves as a container that propagates up through the service layer until intercepted by a global handler.
package com.platform.error;
import lombok.Getter;
import io.swagger.v3.oas.annotations.media.Schema;
@Getter
@Schema(name = "BusinessError", description = "Container for structured business logic errors")
public class BusinessError extends RuntimeException {
private final String code;
private final String clientMessage;
private final String internalDetails;
public BusinessError(String code, String clientMessage, String internalDetails) {
super(internalDetails);
this.code = code;
this.clientMessage = clientMessage;
this.internalDetails = internalDetails;
}
public static BusinessError fromDefinition(ErrorDefinition def) {
return new BusinessError(def.getKey(), def.getClientDesc(), def.getDescription());
}
}
Error Code Definitions
To enforce consistency, error identifiers should be centralized within an enumeration. This approach prevents arbitrary string usage and allows developers to quickly reference valid statuses.
package com.platform.error;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ErrorDefinition {
CLIENT_ERROR_VALIDATION("ERR_01", "Invalid input parameters provided"),
AUTH_DENIED("ERR_AUTH_01", "Authentication credentials rejected"),
ACCOUNT_SUSPENDED("ERR_AUTH_02", "User account has been disabled"),
RESOURCE_MISSING("ERR_RES_01", "Target resource does not exist"),
LIMIT_EXCEEDED("ERR_BIZ_01", "Daily quota limit reached");
private final String key;
private final String clientDesc;
public String getDescription() {
return "Error [" + key + "]: " + clientDesc;
}
}
Global Interceptor Implementation
Spring Boot facilitates centralized error management through the @RestControllerAdvice annotation combined with @ExceptionHandler. This setup allows specific response mapping for distinct exception types without cluttering controller logic.
package com.platform.config;
import com.platform.error.BusinessError;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalApiErrorHandler {
@ExceptionHandler(BusinessError.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleBusinessLogicFailure(BusinessError ex) {
log.warn("Business rule violation occurred: {}", ex.getCode());
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("errorCode", ex.getCode());
result.put("message", ex.getClientMessage());
return result;
}
}
Testing Scenarios
Controllers invoke the custom exception to simulate failures without explicit try-catch blocks. The global advisor intercepts these signals and formats the HTTP 400 response automatically.
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/validate")
public void triggerValidationError() {
// Simulating invalid order status
throw BusinessError.fromDefinition(ErrorDefinition.CLIENT_ERROR_VALIDATION);
}
@PostMapping("/submit")
public String submitOrder() {
boolean isStockAvailable = false;
if (!isStockAvailable) {
throw new BusinessError("ERR_BIZ_01", "Cannot complete purchase", "Inventory depleted");
}
return "Processing...";
}
}
When a request hits /api/orders/validate, the framework detects the BusinessError. Instead of displaying a Java stack trace to the client, the system returns the pre-defined JSON structure containing the error code and message, allowing the frontend to display a friendly notification.