Каждая точка входа в приложение получает данные снаружи — и они могут быть неверными. Вопрос не в том, проверять ли данные, а в том, в каком слое и с какой целью.
Два уровня: граница и домен
Для валидации важны два места:
- Граница (HTTP-контроллер, Kafka-потребитель, gRPC-обработчик) — первая точка, где данные появляются в системе.
- Домен (сервис, агрегат, доменный объект) — место, где данные обретают бизнес-смысл.
У каждого уровня своя задача. Путаница возникает, когда одни и те же проверки размазывают по всем слоям без разбора.
Что проверяем на границе
На границе данные ещё «сырые» — строки, числа, флаги из HTTP-тела или очереди. Здесь уместны структурные проверки:
- обязательность полей:
@NotNull,@NotBlank; - размеры и диапазоны:
@Size,@Min,@Max; - формат:
@Email,@Pattern.
public record CreateOrderRequest(
@NotBlank String customerId,
@NotEmpty List<@NotNull Long> productIds,
@Min(1) int quantity
) {}
@PostMapping("/orders")
public ResponseEntity<OrderResponse> create(
@Valid @RequestBody CreateOrderRequest request) {
...
}
@Valid заставляет Spring проверить поля DTO до вызова метода контроллера. При ошибке — MethodArgumentNotValidException, который @RestControllerAdvice превращает в ответ 422.
Принцип: формат, обязательность, диапазон — на границе.
Что проверяем в домене
Часть правил нельзя проверить по одному полю. Они требуют знания контекста: состояния агрегата, данных из базы, бизнес-политик.
Примеры доменной валидации:
- заказ нельзя отменить, если он уже отгружен;
- скидка не может превышать стоимость корзины;
- покупатель не может купить больше одной единицы товара по акционной цене.
Такие правила живут в домене рядом с состоянием, которое они защищают:
public class Order {
public void cancel() {
if (status == OrderStatus.SHIPPED) {
throw new DomainException("Нельзя отменить отгруженный заказ");
}
this.status = OrderStatus.CANCELLED;
}
}
Принцип: бизнес-инварианты — в домене.
Почему не надо дублировать проверки
Соблазн понятен: продублировать @NotNull в сервисе «на всякий случай». Но это приводит к:
- размытой ответственности — непонятно, кто отвечает за проверку;
- расхождению правил — рано или поздно они начнут противоречить друг другу;
- дублированию кода — одно и то же условие в трёх местах.
Короткая формула: граница проверяет форму данных, домен проверяет смысл данных.
Fail fast: ошибка на входе, а не в глубине
Структурные ошибки лучше обнаружить как можно раньше — до обращения к базе, до вызова внешних сервисов, до создания транзакции. Это называют fail fast.
Если @Valid стоит на контроллере, некорректный запрос отвергается немедленно — без лишней работы. Если проверку пропустить на границе, она всплывёт позже: NullPointerException в репозитории, ошибка внешнего API или повреждённые данные в базе — и отладить это намного сложнее.
Коротко
- Граница (контроллер) проверяет формат, обязательность, диапазоны через
@Validи аннотацииjakarta.validation. - Домен держит бизнес-инварианты — правила, которые зависят от состояния и контекста.
- Дублировать одни и те же проверки во всех слоях вредно: логика расходится, код раздувается.
- Fail fast: структурные ошибки отсекай на входе, не в глубине стека.
- Доменные исключения и HTTP-ошибки — разные вещи; маппинг происходит в
@RestControllerAdvice.
Что почитать дальше
- Bean Validation: @NotNull, @Size, @Valid — стандартные аннотации и как их применять
- Кастомные сообщения и аннотации — собственные правила и локализованные тексты ошибок
- Ошибки REST API — как 422 и другие коды ошибок возвращаются клиенту