Adventuretube REST API Architecture Principle

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

LayerResponsibility
ControllerHandle HTTP interface
ServiceContain domain logic and validation
Exception HandlerCentralize 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<MemberDTO> 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<MemberDTO> 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<RestAPIResponse> 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-servicemember-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<RestAPIResponse> 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<RestAPIResponse> 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:

ScenarioExample
Most application errorsValidation failures, not found, conflicts, etc.
Business rule exceptionsDuplicate user, invalid credentials, token expired
Unexpected server errorsDB timeout, IO error, downstream service failure
RESTful responsesAll 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 *