Приходилось ли заниматься централизованной обработкой исключений
Что проверяют
Понимаете ли вы, зачем превращать исключение в ответ в одном месте, и знаете ли границы этого механизма. За вопросом обычно стоит опыт с однообразным контрактом ошибок и с тем, что наружу не должно уезжать лишнего.
Как отвечать
В Spring основа это класс с @RestControllerAdvice и методы под @ExceptionHandler. Доменные исключения при этом ничего не знают про HTTP: сопоставление со статусом живёт в обработчике, а не в сервисе.
Опишите, что отдаёте наружу. Удобно опираться на формат ProblemDetail: тип ошибки, краткое описание, статус и свои поля, например список непрошедших проверку атрибутов. Обязательная часть это идентификатор запроса в ответе и в логе, чтобы по жалобе пользователя находить конкретную запись.
Разделите ошибки по смыслу: нарушенная валидация даёт MethodArgumentNotValidException и статус из четырёхсотого семейства, отсутствие сущности даёт 404, конфликт версий 409, а всё неопознанное 500 с коротким текстом без подробностей.
Отдельно назовите пределы механизма, за это ставят плюс. Обработчик ловит только то, что дошло до слоя контроллеров: исключение в сервлетном фильтре или в цепочке безопасности до него не доходит, а ошибка внутри асинхронной задачи вообще возникает в другом потоке и обрабатывается своими средствами.
Где спотыкаются
Вешают один обработчик на базовый класс исключений и отдают всё пятисотым ответом. Клиент не может отличить свою ошибку от поломки сервиса, и решение, повторять запрос или нет, принять невозможно.
Второе: забывают про фильтры и про асинхронные задачи. Отказ аутентификации возвращается стандартной страницей в другом формате, и клиент, ожидающий согласованный вид ошибки, падает на разборе ответа.
Третье: кладут в ответ текст исключения целиком. Наружу уезжают имена таблиц и куски запроса, а в логах та же ошибка пишется дважды, потому что её уже записали на месте и повторно в обработчике.