Designing and Implementing Custom Business Exceptions in Spring Boot

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.

Posted on Tue, 19 May 2026 01:14:30 +0000 by livepjam