← назад к методологии · уровень 2 из 3 · предыдущий: уровень 1

Какую проблему решает

На Уровне 1 бизнес-операции не выделены явно: они растворены в сервис-классах с десятками методов. Через полгода непонятно, что сервис умеет, проверки расходятся между вызовами, метрики и аудит прикручиваются руками.

Уровень 2 вводит одну дисциплину: каждую бизнес-операцию выделить в отдельный UseCase + Handler. Этого достаточно, чтобы не утонуть в сервис-классах и получить единый список операций сервиса.

Когда подходит

  • Сервис с заметным числом операций, который будет жить и развиваться.
  • Нужны автоматические метрики, аудит и единая обработка ошибок на каждую операцию.
  • Хочется тестировать бизнес-операцию изолированно, без web-слоя.
  • Команда работает по Use Case спецификации — операции из спеки ложатся в UseCase один-в-один.

Что должно быть

Каждая бизнес-операция — отдельный UseCase + Handler. Создание заказа — один UseCase. Получение заказа по id — другой. Не «один OrderService с пятью методами» — пять отдельных пар.

Один общий маршрут на все операции. Контроллер не знает, какой обработчик вызовется — отдаёт UseCase в диспетчер, тот находит handler по типу. Диспетчер встроен в библиотеку.

Одна модель данных (по умолчанию). То, что приходит из API, и то, что лежит в БД, — одно и то же или связано простым маппингом. Отдельные доменные модели и Value Object — это Уровень 3.

Транзакция на уровне Handler. Один @Transactional на handler — всё или ничего.

Метрики на каждый UseCase автоматически — сколько раз вызвана операция, сколько падала, как долго работала. Этим занимается библиотека.

Как это выглядит

  • Каждая операция — два класса: <Operation>UseCase (record с входными полями) и <Operation>UseCaseHandler (метод handle + аннотация Spring-компонента).
  • Контроллер: распарсил запрос → передал в диспетчер → завернул ответ.
  • Бизнес-проверки и обращения к БД — внутри handler, нигде больше.
  • Тесты — на handler, без mock-инфраструктуры контроллера и репозитория.

CQRS — опция этого уровня

CQRS (Command Query Responsibility Segregation) — не отдельный уровень зрелости, а опция Уровня 2. Включается, когда чтение и запись начинают мешать друг другу: длинные SELECT блокируют, запросам нужен агрессивный кэш, а записям — строгие транзакции.

Явное разделение по типу операции. Команды реализуют маркер UseCaseCommand (меняют состояние), запросы — UseCaseQuery (только читают). Это видно в типе и проверяется в обзоре.

Read Model — отдельная модель данных для чтения. Запросы идут не к той же таблице, что команды, а к оптимизированному представлению: материализованное view, denormalized-таблица, проекция в Elasticsearch, кэш в Redis.

Разные политики на handler-ах. Командные — @Transactional (read-write). Query — @Transactional(readOnly = true) или без транзакции, плюс @Cacheable.

Команды не возвращают «толстые» DTO — идентификатор, минимальный summary или UseCaseEmptyResult. За полными данными клиент идёт отдельным запросом, иначе смысл CQRS теряется.

Read Model строится из write-side — обычно через события (Outbox + relay), реже через материализованные view или ленивый rebuild. Это даёт eventual consistency: запрос может вернуть слегка устаревшие данные — фиксируйте это в спеке («UI видит изменения с задержкой до N секунд»).

Что НЕ нужно делать на этом уровне

  • Заводить агрегаты, value objects и доменные события — это Уровень 3.
  • Строить порты и адаптеры (core / adapter) — тоже Уровень 3.
  • Использовать Event Sourcing — отдельная сложность, не путать с CQRS.

Библиотеки и инструменты

ЧтоЧем
Каркас UseCase + Handler + Dispatcherusecase-pattern-starter
Маркеры UseCaseCommand / UseCaseQuery (CQRS)usecase-pattern-starter (пакет ru.vikulinva.usecase.cqrs)
Web-слойSpring Boot, OpenAPI Codegen
АвторизацияSpring Security + OAuth2
ХранилищеjOOQ или JPA
Read Modelматериализованные view PostgreSQL, Redis, Elasticsearch
Маппинг моделей (при CQRS)MapStruct
МетрикиMicrometer (теги usecase.kind=command\|query)

Признаки, что пора на уровень 3

  • Бизнес-правил много, и они расползаются по handler-ам — проверки копипастятся.
  • В команде заводят разные термины для одного и того же — нужен общий язык домена.
  • Появляются явные инварианты («нельзя оплатить неподтверждённый заказ», «остаток не может быть отрицательным»), и хочется, чтобы они жили в одном месте.
  • Модули логически самодостаточны — намечаются Bounded Context-ы.

Тогда — Уровень 3: DDD + Hexagonal.

Что почитать рядом