7.3 Designing Custom Business Exception Classes
During application development, it's often difficult to express specific business policy violation situations using only Java's standard exceptions (IllegalArgumentException, NullPointerException).
To clearly express and handle situations like "User account not found," "Product out of stock," or "The post has already been deleted," we create Custom Business Exceptions.
Creating a Custom Exception Class
The recommended basic principle when creating exceptions in a Spring backend is to inherit from RuntimeException (Unchecked Exception). This is because the Exception class forced by the compiler requires messy throws declarations throughout the service/controller logic and can also cause issues with default transaction rollback settings.
public class UserNotFoundException extends RuntimeException {
private final String email;
public UserNotFoundException(String email) {
super("User registered with this email could not be found: " + email);
this.email = email;
}
public String getEmail() {
return email;
}
}
If you want to handle numerous business exceptions within the project as a group, you can design a structure with a top-level custom exception class.
// Parent class for all business exceptions
public abstract class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
Defining ErrorCode using Enum
We frequently use an Error Code Enum to manage the error code (e.g., 400, 404), an application-specific consistent error code (e.g., USER_001), and a response message as a single unit.
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public enum ErrorCode {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "U-001", "User not found."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "U-002", "Email already exists."),
OUT_OF_STOCK(HttpStatus.BAD_REQUEST, "I-001", "Product is out of stock.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
ErrorCode(HttpStatus httpStatus, String code, String message) {
this.httpStatus = httpStatus;
this.code = code;
this.message = message;
}
}
Integration with ControllerAdvice
Now we can intercept our business exception hierarchy all at once in @RestControllerAdvice and return it nicely to the client.
@ExceptionHandler(BusinessException.class)
protected ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
ErrorCode errorCode = e.getErrorCode();
ErrorResponse response = new ErrorResponse(errorCode.getCode(), errorCode.getMessage());
return ResponseEntity.status(errorCode.getHttpStatus()).body(response);
}
This approach categorizes errors and provides a clear and consistent error response specification to the collaborating frontend team, making it a very popular pattern in practice.
Recommended Exception Hierarchy
In real-world projects, managing exceptions by layering them as follows is beneficial for maintainability:
BusinessException(Top-level abstract class)EntityNotFoundException(Data not found category)UserNotFoundExceptionPostNotFoundException
InvalidValueException(Invalid value category)DuplicateEmailException
AccessDeniedException(Access denied category)
By layering this way, handling just the parent type BusinessException in @RestControllerAdvice allows you to process all underlying custom exceptions with a common logic.
🎯 Key Points
- Custom exceptions are used to clearly communicate business intent.
- Inherit from RuntimeException to prevent unnecessary exception propagation declarations.
- Combining them with an ErrorCode Enum makes error management highly systematic.