Danh mục:
Trong phát triển ứng dụng API, thiết kế ngoại lệ (exception design) là một phần thường bị lùi lại. Nhưng nếu thiết kế sai, khi hệ thống mở rộng, cấu trúc ngoại lệ sẽ càng phức tạp và gây ảnh hưởng tới: cấu trúc phản hồi API, ghi log, giám sát lỗi…
Bài viết này trình bày:
- Các mẫu thiết kế ngoại lệ thường gây lỗi.
- Một cách thiết kế ngoại lệ có cấu trúc rõ ràng, dễ mở rộng dựa vào:
- Phân tách theo Server / Client
- Sử dụng
enum ErrorCode - Sử dụng
ResolvervàFactory Methodđể tạo ngoại lệ.
1) Các mẫu ngoại lệ hay gặp nhưng có vấn đề
🔹 Theo domain quá chi tiết
Nhiều dự án phân loại ngoại lệ theo danh mục nghiệp vụ, ví dụ:
public class AuthException extends RuntimeException { … } public class UserException extends RuntimeException { … }
Rồi sau đó lại tiếp tục tách một cách quá chi tiết:
public class InvalidLoginAccountException extends AuthException { … } public class NoUserException extends UserException { … }
Khi đó lớp ngoại lệ mỗi domain càng ngày càng nhiều, khó quản lý, khó biết ý nghĩa và gán mã HTTP phù hợp.
2) Vấn đề khi ngoại lệ quá trừu tượng
Khi gom tất cả ngoại lệ thành một lớp duy nhất như:
public class ApiException extends RuntimeException { private final int httpStatus; … }
Lớp domain chứa kiến thức về
HTTP, khiến domain lệ thuộc giao thức cụ thể. Nếu muốn dùng cùng domain cho gRPC, CLI, microservice khác… sẽ phải chỉnh sửa lại. 3) Cách thiết kế đề xuất
Tách theo Server / Client + ErrorCode✔️ Bước 1: Định nghĩa hai loại ngoại lệ chính
1) ClientException
Dùng cho lỗi do người dùng/gửi request sai.
2) ServerException
Dùng cho lỗi phía server / lỗi hệ thống.
📌 Cả hai lớp này đều kế thừa một lớp chung DomainException:
public class DomainException extends RuntimeException { private final ErrorCode code; private final ErrorKind kind; // … }
Trong đó:
ErrorKind chỉ ra Client hay Server.ErrorCode là enum chứa mã lỗi xác định, ví dụ:INVALID_ARGUMENT, VALIDATION_FAILED,UNAUTHORIZED, USER_NOT_FOUND,EXTERNAL_API_FAILURE, DB_FAILURE, … 📌 Bước 2: Sử dụng
Factory Method để tạo ngoại lệThay vì tạo ngoại lệ trực tiếp trong code, ta sử dụng các factory để sinh ngoại lệ theo quy chuẩn:
Ví dụ: UserErrors.java
public static ClientException userNotFound(long userId) { // gán ErrorCode.USER_NOT_FOUND, message, metadata… return new ClientException(ErrorCode.USER_NOT_FOUND, "user not found: id=" + userId, metadata); }
java
Khi cần dùng:
User user = userRepository.findById(userId)
.orElseThrow(() -> UserErrors.userNotFound(userId));
📌 Điều này:
- Giữ cho mã nghiệp vụ sạch và chỉ thể hiện ý định (intent).
- Tập trung thông tin giá trị ErrorCode/message ở nơi duy nhất.
4) Mapper ngoại lệ thành phản hồi API
ErrorResponse DTO
Khái niệm một lớp phản hồi lỗi chung:
public class ErrorResponse { private final String code; private final int httpStatus; private final String description; private final String resourceUri; private final String traceId; private final String timestamp; private final Map<String, Object> details; private final Boolean retryable; }
Các trường này dùng để:
- Trả giá trị mã lỗi (“code”),
- Trạng thái HTTP tương ứng,
- Thông tin chi tiết nếu cần,
- Tích hợp trace/logging…
HttpErrorResponseResolver
- Đây là lớp chịu trách nhiệm chuyển DomainException → ErrorResponse:
- Dựa vào ErrorKind & ErrorCode để chọn HTTP status phù hợp.
- Gán retryable nếu lỗi server có thể thử lại (ví dụ: external API bị lỗi).
5) Global Exception Handling
Trong Spring Boot, bạn có thể dùng:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(DomainException.class) public ResponseEntity<ErrorResponse> handleDomainException(...)
- Để bắt DomainException và trả JSON theo chuẩn ErrorResponse.
- Ngoài ra, cũng xử lý các Exception không lường trước bằng phản hồi “UNEXPECTED_ERROR”.
Tóm tắt
🔹 Thiết kế ngoại lệ tốt giúp:
- Giảm độ phức tạp khi hệ thống lớn.
- Tách biệt kiến thức domain khỏi HTTP.
- Phản hồi API nhất quán và dễ hiểu cho client.
- Quản lý lỗi dễ mở rộng và dễ debug.
Nguồn bài viết dịch từ ryukato.github.io