Adventuretube REST API Exception Handling

1. Correct Design of Controller and Service

Principle: Thin Controller, Fat Service

  • Controllers should only handle HTTP concerns: routing, request/response formatting, and returning status codes.
  • Services should encapsulate business logic, including all validations and error throwing.

Why This Matters

1. Separation of Concerns

Layer Responsibility
Controller Handle HTTP interface
Service Contain domain logic and validation
Exception Handler Centralize error-to-response mapping

2. Centralized Error Handling

  • Services throw exceptions like DuplicateException(AuthErrorCode.USER_EMAIL_DUPLICATE)
  • Global exception handler converts that into a uniform API error response

3. Thin Controllers Are Easier to Maintain

// Recommended
@PostMapping("register")
public ResponseEntity register(@RequestBody MemberDTO dto) {
    Member member = service.register(dto);
    return ResponseEntity.ok(dto);
}
// Avoid
@PostMapping("register")
public ResponseEntity register(@RequestBody MemberDTO dto) {
    try {
        service.register(dto);
        return ResponseEntity.ok(dto);
    } catch (Exception e) {
        return ResponseEntity.status(500).body("Error");
    }
}

2. Sequence to Create Custom Exception Handling

Step-by-Step Workflow

Step 1: Define AuthErrorCode Enum

public enum AuthErrorCode {
    USER_EMAIL_DUPLICATE("User already exists", HttpStatus.CONFLICT),
    USER_NOT_FOUND("User not found", HttpStatus.NOT_FOUND),
    // ... other error codes
    ;

    private final String message;
    private final HttpStatus httpStatus;

    AuthErrorCode(String message, HttpStatus status) {
        this.message = message;
        this.httpStatus = status;
    }

    public String getMessage() { return message; }
    public HttpStatus getHttpStatus() { return httpStatus; }
}

Step 2: Create Custom Exception

public class DuplicateException extends RuntimeException {
    private final AuthErrorCode errorCode;

    public DuplicateException(AuthErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }

    public AuthErrorCode getErrorCode() {
        return errorCode;
    }
}

Step 3: Update Service Layer to Throw Exception

public Member registerMember(Member member) {
    if (repository.findByEmail(member.getEmail()).isPresent()) {
        throw new DuplicateException(AuthErrorCode.USER_EMAIL_DUPLICATE);
    }
    return repository.save(member);
}

Step 4: Refactor Controller to Handle Only Success

@PostMapping("registerMember")
public ResponseEntity registerMember(@RequestBody MemberDTO memberDTO) {
    Member member = mapper.toEntity(memberDTO);
    Member registered = memberService.registerMember(member);
    memberDTO.setId(registered.getId());
    return ResponseEntity.ok(memberDTO);
}

Step 5: Global Exception Handler

@ExceptionHandler(DuplicateException.class)
public ResponseEntity handleDuplicate(DuplicateException ex) {
    return ResponseEntity
        .status(ex.getErrorCode().getHttpStatus())
        .body(new RestAPIResponse(
            ex.getErrorCode().getMessage(),
            ex.getErrorCode().name(),
            ex.getErrorCode().getHttpStatus().value(),
            System.currentTimeMillis()
        ));
}

3. Error Handling Between Microservices

Strategy for Service-to-Service Communication

Goal: Ensure service calls (e.g., auth-service -> member-service) result in structured, traceable, and meaningful error handling.

Best Practices

  • Always use ResponseEntity<RestAPIResponse> even for simple internal endpoints (no raw boolean returns).
  • Throw domain-specific exceptions in each service and convert them into consistent error responses using global handler.
  • Use AuthErrorCode or a shared ErrorCode library across services to standardize error definitions.

Example

In member-service (token deletion)

@PostMapping("deleteAllToken")
public ResponseEntity deleteAllToken(@RequestBody String token) {
    boolean deleted = memberService.deleteAllToken(token);
    if (!deleted) {
        throw new TokenDeletionException(AuthErrorCode.TOKEN_DELETION_FAILED);
    }
    return ResponseEntity.ok(RestAPIResponse.success("All tokens deleted successfully"));
}

In auth-service

public void logout(HttpServletRequest request) {
    ResponseEntity response = memberClient.deleteAllToken(token);
    if (response.getStatusCode() != HttpStatus.OK) {
        throw new TokenDeletionException(AuthErrorCode.TOKEN_DELETION_FAILED);
    }
    // proceed with logout
}

Benefits

  • Error messages are consistent across services
  • Easier to debug and trace failures
  • Structured JSON responses for monitoring and logging
  • Seamless integration with Swagger/OpenAPI and frontend error handling

4. Things I Did Care Using Global Exception Handler

When to Use Global Exception Handler:

Scenario Example
Most application errors Validation failures, not found, conflicts, etc.
Business rule exceptions Duplicate user, invalid credentials, token expired
Unexpected server errors DB timeout, IO error, downstream service failure
RESTful responses All API-facing logic should go through it

These should always be handled centrally via @ControllerAdvice to ensure consistency and maintainability.


5. The Value of Structured Error Architecture

While it may seem like extra effort to create custom exceptions and enum-based error codes for cases as simple as “Failed to register member,” what you’re actually building is a resilient, maintainable, and enterprise-grade error handling system.

Here’s what you’re gaining:

1. Strong Typing

You’re not just passing a string — you’re throwing a typed, predictable error object. This makes testing and exception handling logic more reliable and maintainable.

2. Consistent HTTP Semantics

Every AuthErrorCode carries an appropriate HTTP status code. You no longer need to manually assign status codes in each controller or exception.

3. Centralized Vocabulary of Errors

Your AuthErrorCode enum becomes a living catalog of all failure modes in the system. This is great for documentation, onboarding, and Swagger/OpenAPI support.

4. Future i18n/Localization Readiness

You can easily map AuthErrorCode names to translated messages in frontend or external error files.

5. Better Logging, Monitoring, and Debugging

Logs and structured JSON error responses now include error codes, timestamps, and context — making it easier to debug issues across distributed systems.

6. Testability

You can now write assertions like:

assertEquals(AuthErrorCode.TOKEN_SAVE_FAILED, ex.getErrorCode());

Rather than relying on fragile message string matching.


In summary: this architecture doesn’t just help you return better errors — it builds the foundation for reliable, scalable, and observable service-to-service communication across your REST API ecosystem.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top