← назад к разделу

Каждая точка входа в приложение получает данные снаружи — и они могут быть неверными. Вопрос не в том, проверять ли данные, а в том, в каком слое и с какой целью.

Два уровня: граница и домен

Для валидации важны два места:

  • Граница (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 и другие коды ошибок возвращаются клиенту