Exception handling mechanism prompt for spring boot project
## Exception Mechanism — Prompt (Spring Boot Microservice)
### Goal
Build a standardized, i18n-enabled exception mechanism in a Spring Boot microservice that returns error responses aligned with HTTP status codes.
---
### Package structure
```
exception/
├── base/
│ ├── BaseErrorService.java (interface)
│ ├── BaseErrorEnum.java (common/base error codes)
│ ├── BaseException.java (abstract exception)
│ ├── BaseErrorResponseDto.java (API response DTO)
│ └── {Specific}Exception.java (optional — domain-specific, extends BaseException)
├── {ServiceName}ErrorEnum.java (project-specific error enum)
├── {ServiceName}Exception.java (general domain exception)
├── {Domain}Exception.java (domain exceptions — as needed)
├── ClientException.java (Feign/external error wrapper — SEPARATE from BaseException)
└── GlobalExceptionHandler.java (@RestControllerAdvice)
```
---
### 1. `BaseErrorService` (interface)
The contract that all error enums must implement:
```java
public interface BaseErrorService {
String getMessage(); // user-facing message (resolved from i18n)
int getHttpStatus(); // HTTP status code (400, 404, 409, 500, etc.)
String getErrorCode(); // unique error code (e.g. "CUSTOMER_NOT_FOUND_ERROR-0003")
}
```
---
### 2. `BaseErrorEnum` (common/base error enum)
**5 common errors** that can stay the same across microservices and are not tied to a specific project:
| Enum constant | Error Code | i18n Key | HTTP Status |
|---|---|---|---|
| BASE_BUSINESS_ERROR | BASE-BUSINESS-ERROR-001 | BASE_BUSINESS_ERROR | 400 |
| BASE_TECH_ERROR | BASE-TECH-ERROR-001 | BASE_TECH_ERROR | 500 |
| BASE_GATEWAY_TIMEOUT | BASE-GATEWAY-TIMEOUT-001 | BASE_GATEWAY_TIMEOUT | 504 |
| BASE_SERVER_ERROR | BASE-SERVER-ERROR-001 | BASE_SERVER_ERROR | 503 |
| BASE_VALIDATION_ERROR | BASE-VALIDATION-ERROR-001 | BASE_VALIDATION_ERROR | 400 |
Each enum entry takes 3 parameters: `(errorCode, messageKey, httpStatus)`.
The `getMessage()` method calls `MessageResolverUtil.getMessage(messageKey)` — so the message is not a plain string, but an **i18n properties file key**.
---
### 3. Project-specific Error Enum (`{ServiceName}ErrorEnum`)
Implements `BaseErrorService`. Same structure as `BaseErrorEnum`, but:
- Base errors are **duplicated** (same 5 entries)
- **Domain-specific errors** are added on top
Example entries:
```
UNAUTHORIZED_ERROR("UNAUTHORIZED-ERROR-0001", "UNAUTHORIZED_ERROR", 400)
RESOURCE_NOT_FOUND_ERROR("RESOURCE_NOT_FOUND_ERROR-0004", "RESOURCE_NOT_FOUND_ERROR", 404)
RESOURCE_ALREADY_EXIST_ERROR("RESOURCE_ALREADY_EXIST_ERROR-0006", "RESOURCE_ALREADY_EXIST_ERROR", 409)
ACCESS_DENIED_ERROR("ACCESS_DENIED_ERROR-0020", "ACCESS_DENIED_ERROR", 403)
```
**Rules:**
- `errorCode` — unique, format: `{ERROR_NAME}-{4-digit number}`
- `message` — key in i18n properties file (usually same as enum name)
- `httpStatus` — aligned with REST semantics (404 not found, 409 conflict, 403 forbidden, 400 bad request)
---
### 4. i18n (MessageSource) configuration
**`ResourceBundleConfig`:**
```java
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:i18n/error/messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
```
**Properties files:** `src/main/resources/i18n/error/`
- `messages_en.properties`
- `messages_az.properties`
- `messages_ru.properties`
Format: `ERROR_KEY=Translated message text`
Example:
```properties
RESOURCE_NOT_FOUND_ERROR=Resource not found
ACCESS_DENIED_ERROR=Access denied
BASE_SERVER_ERROR=A technical error beyond our control has occurred.
```
**`MessageResolverUtil`:**
- Spring `@Component`, `MessageSource` injected in constructor
- Static `getMessage(String key)` method — returns message for current locale via `LocaleContextHolder.getLocale()`
- If key is not found, returns the key itself as fallback
---
### 5. `BaseException` (abstract class)
Extends `RuntimeException`. Parent class for all domain exceptions:
```java
public abstract class BaseException extends RuntimeException {
public final Object[] args; // for future parameterized i18n messages
public final BaseErrorService baseErrorService; // error metadata
// 2 constructors:
BaseException(BaseErrorService baseErrorService, Object... args)
BaseException(BaseErrorService baseErrorService, Throwable throwable, Object... args)
}
```
Constructor calls `super(baseErrorService.getMessage())` — so the exception's `getMessage()` returns the i18n-resolved message.
---
### 6. Domain Exception classes
All extend `BaseException` and are simple wrappers:
| Exception | Purpose |
|---|---|
| `{ServiceName}Exception` | General domain errors (most commonly used) |
| `{DomainA}Exception` | Errors for a specific domain/business area |
| `{DomainB}Exception` | Errors for another domain/business area |
| `{Specific}Exception` | Specific error type (optional, can live in base package) |
Each constructor: `(BaseErrorService errorService, Object... args)`
**Usage in service layer:**
```java
throw new {ServiceName}Exception(RESOURCE_NOT_FOUND_ERROR);
throw new {DomainA}Exception(RESOURCE_ALREADY_EXIST_ERROR);
throw new {ServiceName}Exception(BaseErrorEnum.BASE_SERVER_ERROR); // generic error can also be used
```
---
### 7. `BaseErrorResponseDto` (API response record)
```java
public record BaseErrorResponseDto(
String code, // error code
String message, // user-facing message
String path, // request path
String timestamp, // LocalDateTime.now().toString()
Integer status, // HTTP status
Object... data // extra data (Map<String,String> for validation errors)
) {}
```
Used both as the **API response format** and to **parse errors from Feign clients**.
---
### 8. `GlobalExceptionHandler` (@RestControllerAdvice)
Catches all exceptions and returns a standardized `BaseErrorResponseDto` response.
**Handlers:**
| Handler | Exception type | Notes |
|---|---|---|
| `handleBaseException` | `BaseException` | Generic handler — exceptions without a dedicated handler fall here |
| `handleValidationException` | `MethodArgumentNotValidException` | Validation error — field errors returned as Map in `data`, uses `BASE_VALIDATION_ERROR` code |
| `handle{ServiceName}Exception` | `{ServiceName}Exception` | Domain exception handler |
| `handle{DomainA}Exception` | `{DomainA}Exception` | Domain exception handler (optional) |
**Each handler follows the same pattern:**
```java
return new ResponseEntity<>(
new BaseErrorResponseDto(
ex.baseErrorService.getErrorCode(),
ex.getMessage(),
webRequest.getContextPath(),
LocalDateTime.now().toString(),
ex.baseErrorService.getHttpStatus()
),
HttpStatusCode.valueOf(ex.baseErrorService.getHttpStatus())
);
```
**Validation handler is special:** passes a field→error message Map in the `data` parameter.
---
### 9. `ClientException` (Feign/external error — SEPARATE hierarchy)
Does **not** extend `BaseException`. Wraps errors from external services:
```java
public class ClientException extends RuntimeException {
private final transient BaseErrorResponseDto baseErrorResponse;
}
```
**No handler in `GlobalExceptionHandler`** — this exception is caught in the service layer and does not propagate directly to the API.
---
### 10. `FeignClientErrorDecoder` (Feign error handling)
Parses Feign client error responses:
1. Reads response body as `BaseErrorResponseDto`
2. If external APIs use a different error format, maps them to `BaseErrorResponseDto`
3. Returns `ClientException(errorDetail)`
4. If parsing fails → fallback to `{ServiceName}Exception(BaseErrorEnum.BASE_SERVER_ERROR)`
**ClientException handling in service layer:**
```java
String errorDetail = switch (e) {
case ClientException ce -> ce.getBaseErrorResponse().message();
case {ServiceName}Exception se -> se.baseErrorService.getMessage();
default -> e.getMessage();
};
```
---
### Full flow diagram
```
[Service Layer]
│
├─ throw new {ServiceName}Exception(ERROR_ENUM)
│ ↓
│ [GlobalExceptionHandler]
│ ↓
│ BaseErrorResponseDto { code, message, path, timestamp, status }
│ ↓
│ HTTP Response (404/400/409/...)
│
└─ feignClient.someCall()
↓ (error response)
[FeignClientErrorDecoder]
↓
ClientException(BaseErrorResponseDto)
↓
[Service catch block]
↓
error detail stored / mapped to domain exception
```
---
### Checklist for setting up in a new project
1. **Create `exception/base/` package:**
- `BaseErrorService` interface
- `BaseErrorEnum` (5 common errors)
- `BaseException` abstract class
- `BaseErrorResponseDto` record
2. **Create project-specific error enum** (`{ServiceName}ErrorEnum implements BaseErrorService`)
3. **Configure i18n:**
- `ResourceBundleConfig` → `MessageSource` bean
- `MessageResolverUtil` → static message resolver
- `messages_en/az/ru.properties` files
4. **Create domain exception classes** (extend `BaseException`)
5. **Create `GlobalExceptionHandler`** (`@RestControllerAdvice`):
- Generic `BaseException` handler
- `MethodArgumentNotValidException` validation handler
- Optional dedicated handlers for domain exceptions (same pattern)
6. **Feign error handling (optional):**
- `ClientException` (SEPARATE from `BaseException`)
- `FeignClientErrorDecoder` → parse response into `BaseErrorResponseDto`
7. **Usage in service layer:**
```java
throw new {Domain}Exception({ServiceName}ErrorEnum.SOME_ERROR);
```
---
Comments
Post a Comment