🏗️ Part 2: Architecture – Custom Exception Design & Global Handling
🧭 Series Navigation:
- 🏠 Exception Handling Hub
- ▶️ Part 1: Foundation
- ✅ Part 2: Architecture (Current)
- ▶️ Part 3: Implementation
- ▶️ Part 4: Production Guide
🏗️ Exception Architecture
Building Robust Exception Handling with Custom Classes & Global Management
🎯 What You’ll Learn
- BaseServiceException with automatic origin tracking
- Service-specific exception design patterns
- GlobalExceptionHandler centralized management
- ServiceResponse wrapper for unified API responses
🏗️ Architecture Overview
A well-architected exception handling system has four key components:
🧱 BaseServiceException
Generic foundation with origin tracking
🏷️ Service-Specific Exceptions
Typed exceptions for different services
🌐 GlobalExceptionHandler
Centralized exception management
🎁 ServiceResponse Wrapper
Unified API response format
🧱 Base Exception Architecture
Generic Base Class with Origin Tracking
@Getter
public abstract class BaseServiceException extends RuntimeException {
private final ErrorCode errorCode;
private final String origin;
private final LocalDateTime timestamp;
public BaseServiceException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
// Automatically capture className.methodName for debugging
StackTraceElement[] stackTrace = this.getStackTrace();
this.origin = stackTrace.length > 0
? stackTrace[0].getClassName() + "." + stackTrace[0].getMethodName()
: "UnknownOrigin";
}
public BaseServiceException(ErrorCode errorCode, String customMessage) {
super(customMessage);
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
StackTraceElement[] stackTrace = this.getStackTrace();
this.origin = stackTrace.length > 0
? stackTrace[0].getClassName() + "." + stackTrace[0].getMethodName()
: "UnknownOrigin";
}
public BaseServiceException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.timestamp = LocalDateTime.now();
StackTraceElement[] stackTrace = this.getStackTrace();
this.origin = stackTrace.length > 0
? stackTrace[0].getClassName() + "." + stackTrace[0].getMethodName()
: "UnknownOrigin";
}
}
💡 Why Origin Tracking Matters
- Rapid debugging: Instantly know where exceptions originated
- Production monitoring: Track exception patterns by service/method
- Team collaboration: Clear ownership of exception sources
- Performance insights: Identify bottlenecks and failure points
🏷️ Service-Specific Exception Classes
Member Service Exceptions
// User-related exceptions
public class UserNotFoundException extends BaseServiceException {
public UserNotFoundException(MemberErrorCode errorCode) {
super(errorCode);
}
public UserNotFoundException(MemberErrorCode errorCode, String email) {
super(errorCode, String.format("User with email '%s' not found", email));
}
}
public class DuplicateException extends BaseServiceException {
public DuplicateException(MemberErrorCode errorCode) {
super(errorCode);
}
public DuplicateException(MemberErrorCode errorCode, String email) {
super(errorCode, String.format("User with email '%s' already exists", email));
}
}
public class ValidationException extends BaseServiceException {
public ValidationException(MemberErrorCode errorCode) {
super(errorCode);
}
public ValidationException(MemberErrorCode errorCode, String field, String value) {
super(errorCode, String.format("Invalid %s: '%s'", field, value));
}
}
// System-related exceptions
public class ServiceException extends BaseServiceException {
public ServiceException(MemberErrorCode errorCode) {
super(errorCode);
}
public ServiceException(MemberErrorCode errorCode, Throwable cause) {
super(errorCode, cause);
}
}
🌐 GlobalExceptionHandler Implementation
Centralized Exception Management
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
private final MeterRegistry meterRegistry;
public GlobalExceptionHandler(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
// Helper method for consistent responses
private ResponseEntity<ServiceResponse<?>> buildErrorResponse(
ErrorCode errorCode, BaseServiceException ex, String serviceName) {
// Log based on severity
logException(ex, errorCode);
// Track metrics
trackExceptionMetrics(errorCode, serviceName);
return new ResponseEntity<>(
ServiceResponse.builder()
.success(false)
.message(ex.getMessage())
.errorCode(errorCode.name())
.data(Map.of(
"origin", ex.getOrigin(),
"service", serviceName,
"timestamp", ex.getTimestamp(),
"severity", errorCode.getSeverity().name()
))
.timestamp(LocalDateTime.now())
.build(),
errorCode.getHttpStatus()
);
}
// 1. DEPENDENCY CHECKS (5xx)
@ExceptionHandler(ServiceException.class)
public ResponseEntity<ServiceResponse<?>> handleServiceException(
ServiceException ex) {
return buildErrorResponse(ex.getErrorCode(), ex, "member-service");
}
// 2. BUSINESS VALIDATION (4xx)
@ExceptionHandler(DuplicateException.class)
public ResponseEntity<ServiceResponse<?>> handleDuplicationException(
DuplicateException ex) {
return buildErrorResponse(ex.getErrorCode(), ex, "member-service");
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ServiceResponse<?>> handleUserNotFoundException(
UserNotFoundException ex) {
return buildErrorResponse(ex.getErrorCode(), ex, "member-service");
}
// 3. UNKNOWN ERRORS (5xx)
@ExceptionHandler(Exception.class)
public ResponseEntity<ServiceResponse<?>> handleUnknownException(Exception ex) {
log.error("Unexpected error occurred", ex);
ServiceException serviceEx = new ServiceException(MemberErrorCode.INTERNAL_ERROR, ex);
return buildErrorResponse(MemberErrorCode.INTERNAL_ERROR, serviceEx, "member-service");
}
}
🎁 ServiceResponse Wrapper
Unified Response Format
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ServiceResponse<T> {
private boolean success;
private String message;
private String errorCode;
private T data;
private LocalDateTime timestamp;
// Factory methods for success responses
public static <T> ServiceResponse<T> success(String message, T data) {
return ServiceResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.timestamp(LocalDateTime.now())
.build();
}
public static <T> ServiceResponse<T> success(T data) {
return ServiceResponse.<T>builder()
.success(true)
.message("Operation completed successfully")
.data(data)
.timestamp(LocalDateTime.now())
.build();
}
// Factory methods for error responses
public static <T> ServiceResponse<T> error(String message, String errorCode) {
return ServiceResponse.<T>builder()
.success(false)
.message(message)
.errorCode(errorCode)
.timestamp(LocalDateTime.now())
.build();
}
// Validation helpers
public boolean isSuccess() {
return success;
}
public boolean isError() {
return !success;
}
}
🎯 Part 2 Key Takeaways
✅ BaseServiceException
- Automatic origin tracking
- Correlation IDs for request tracing
- Contextual information for debugging
✅ ErrorCode Interface
- Severity levels for appropriate logging
- Categories for better classification
- Extensible design for service needs
✅ GlobalExceptionHandler
- Priority-based exception handling
- Consistent response formats
- Automatic metrics tracking
✅ ServiceResponse Wrapper
- Standard success/error format
- Rich error context information
- Easy client-side error handling
🔗 Continue Your Journey
🧭 Series Navigation:
- 🏠 Exception Handling Hub
- ▶️ Part 1: Foundation
- ✅ Part 2: Architecture (Current)
- ▶️ Part 3: Implementation
- ▶️ Part 4: Production Guide