The full picture
- The test: can the immediate caller realistically recover with a different action? If yes (e.g.,
InterruptedException, a retryable payment decline modeled explicitly), checked can earn its keep. If the only realistic response is fail-the-request, unchecked. - Layering rule: never let
SQLExceptioncross a service boundary — translate at the boundary into your domain's hierarchy (OrderNotFound,PaymentDeclined,InfrastructureException). Spring'sDataAccessExceptiontranslation is this principle industrialized. - Wrap, don't swallow:
throw new DomainException("context", cause)— always chain the cause; a lost stack trace turns a 10-minute fix into a day. - Lambdas broke checked:
Stream.map(o -> repo.load(o))won't compile ifloadthrows checked — hence sneaky-throw hacks and wrapper ceremony. New APIs avoid inflicting that.
public Order load(OrderId id) {
try {
return jdbc.queryForObject(SQL, mapper, id.value());
} catch (EmptyResultDataAccessException e) {
throw new OrderNotFoundException(id, e); // domain-speak + cause
}
// infrastructure exceptions bubble as-is; a @ControllerAdvice
// maps: OrderNotFound -> 404, DomainValidation -> 422, rest -> 500
}