Interview Prep Zoneby CuriouserLabs

Question 3 of 5 · architect level

Design an exception-handling strategy for a layered microservice — from repository to HTTP response.

🎤 Say this first

One principle: catch late, throw early, translate at boundaries, handle in exactly one place. Repositories translate driver errors to domain/infra exceptions; the domain throws its own hierarchy; nothing catches what it can't handle; a single top edge (@ControllerAdvice + ProblemDetail) maps the hierarchy to HTTP codes with correlation IDs, and logs each failure once at the edge with full context. Kill the twin sins: log-and-rethrow (duplicate noise) and catch-log-continue (zombie state).

The full picture

  1. Repository/adapter layer: translate technology exceptions (SQL, HTTP client, Kafka) into a small infra hierarchy; attach identifiers, never raw SQL/PII.
  2. Domain layer: explicit business exceptions (InsufficientStock, DuplicateBooking) extending a sealed-ish base; expected alternatives may be result types instead.
  3. Application edge: one @ControllerAdvice: business → 4xx with ProblemDetail body (type, title, correlationId), infra → 502/503 with retry hints, unknown → 500 generic (no internals leaked). Log level policy: 4xx = INFO/WARN, 5xx = ERROR with stack.
  4. Async/batch edges (Kafka listener, scheduler) get the same 'single handler' treatment — DLQ + alert instead of HTTP mapping.
  5. Cross-cutting: correlation ID in MDC from the first filter; error metrics by exception type; alert on rate, not single occurrences.
The single edge
@RestControllerAdvice
class ApiErrors {
    @ExceptionHandler(DomainException.class)
    ProblemDetail domain(DomainException e) {
        var p = ProblemDetail.forStatusAndDetail(e.status(), e.getMessage());
        p.setProperty("correlationId", MDC.get("cid"));
        return p;                                 // RFC 9457 response
    }
    @ExceptionHandler(Exception.class)
    ProblemDetail unknown(Exception e) {
        log.error("unhandled", e);                // logged ONCE, here
        return ProblemDetail.forStatus(500);      // no internals leaked
    }
}

🔄 Likely follow-up questions

  • How do you prevent stack traces/PII leaking into API responses while keeping them in logs?
  • Retryable vs non-retryable: how does the hierarchy communicate that to clients and to your own retry middleware?
  • How does this strategy adapt to event consumers where there is no caller to answer?