Уровень зрелости 3: Доменный слой выделен

Третий уровень Use Case Pattern: бизнес-логика и инварианты живут в доменной модели — агрегаты, value objects, доменные события. Handler становится оркестратором.

Статья внедрена в скилл AI-агента ucp-pattern-review / ucp-pattern-design Эталонная библиотека к статье usecase-pattern Use Case Pattern уровень 3 DDD

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

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

На втором уровне команды и запросы разнесены, но бизнес-правила всё ещё живут в handler-ах. Когда правил становится много, начинаются три беды:

  1. Дублирование проверок. Одно и то же правило проверяется в трёх handler-ах, потому что три операции его затрагивают. Кто-то забывает — баг в проде.
  2. Размытый язык. Один разработчик пишет «order», другой «purchase», третий «sale» — про одно и то же. Бизнес-аналитик в шоке.
  3. Невозможность отрефакторить. Логика «оплатить заказ» собрана из 8 строк проверок и 4 вызовов репозиториев в PayOrderHandler. Никаких границ — каждое изменение рискует уронить три других сценария.

Третий уровень добавляет доменный слой: явные понятия (Order, Customer, Money), их состояния, их инварианты и события, которые они порождают. Бизнес-логика живёт в домене, а не в handler-ах.

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

  • Сложные бизнес-инварианты, которые нельзя нарушать никогда (отрицательный остаток, платёж в неподтверждённом заказе).
  • Доменный язык важен — команда работает с бизнесом плотно, термины перетекают в код.
  • Ясно выделяются Bounded Context-ы: «оформление заказа» и «расчёт с продавцами» — это разные миры со своими правилами.
  • Сервис будет жить и развиваться годами, нужна устойчивость к изменениям бизнес-правил.

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

Aggregate Root — корень бизнес-объекта. Один корень держит все инварианты внутри: Order отвечает за то, что сумма заказа сходится с суммой позиций, что нельзя оплатить отменённый заказ, что нельзя добавить позицию после отправки. Все изменения проходят через методы корня — никаких прямых сеттеров наружу.

Entity — сущности с идентичностью внутри агрегата. Например, позиция заказа — это Entity внутри агрегата Order. Имеет id, может меняться, но только через корень.

Value Object — типы без идентичности, описывающие значение. Money, Email, Quantity, OrderId. Immutable, сравниваются по значению. Они снимают «примитивную одержимость»: вместо BigDecimal amountMoney money, и инварианты денег (валюта, точность) живут в типе.

Domain Event — факт, который произошёл в домене. OrderPaid, OrderShipped, RefundIssued — глаголы в прошедшем времени. События публикуются агрегатом при изменении состояния и потом разъезжаются подписчикам — внутри сервиса или через шину.

Repository — абстракция доступа к агрегатам целиком. Не «таблица заказов» — а «коллекция Order-ов в памяти». Интерфейс в домене, реализация в инфраструктуре.

UseCaseHandler становится оркестратором. Раньше он сам проверял правила. Теперь он: загрузил агрегат → вызвал на нём бизнес-метод → сохранил → опубликовал собранные события. Все «нельзя» — внутри агрегата.

Как это меняет код Handler

До (уровень 2):

public void handle(PayOrderCommand cmd) {
    Order order = repo.find(cmd.orderId());
    if (order.status() != CONFIRMED) throw new InvalidState();
    if (cmd.amount().compareTo(order.total()) < 0) throw new Underpaid();
    order.setStatus(PAID);
    order.setPaidAt(now);
    repo.save(order);
    eventBus.publish(new OrderPaidEvent(order.id(), cmd.amount()));
}

После (уровень 3) — все проверки и публикация события переезжают в Order.pay(...):

public void handle(PayOrderCommand cmd) {
    Order order = repo.findById(cmd.orderId()).orElseThrow();
    order.pay(cmd.amount());     // внутри: проверки, смена статуса, registerEvent(OrderPaid)
    repo.save(order);            // save: персист + публикация событий + clearEvents
}

Handler становится тоньше. Все правила собраны в Order — место, куда смотрит и бизнес-аналитик, и AI-агент, проверяющий код.

Что должно быть зафиксировано в спеке

На третьем уровне Use Case спецификация перерастает в Tier C — добавляются разделы:

  • агрегаты и их инварианты;
  • value objects;
  • доменные события (что публикуется и кто слушает);
  • Bounded Context-ы и связи между ними (ACL, OHS, Conformist).

Раздел «Domain Model» становится опорным.

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

  • Тянуть гексагональную раскладку (core/ + adapter/) — пока живёт в обычной слоистой структуре.
  • Делать Event Sourcing — это отдельная сложность; в большинстве случаев достаточно текущего состояния + Outbox.
  • Делать Saga, если нет реальных межсервисных процессов.

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

ЧтоЧем
Базовые DDD-абстракцииddd-building-blocks: Entity<ID>, AggregateRoot<ID>, ValueObject, DomainEvent, Specification
Каркас UseCaseusecase-pattern-starter
Маппинг между слоямиMapStruct (JsonBean ↔ доменная модель ↔ Pojo)
Атомарная публикация событийTransactional Outbox + Outbox-relay (см. распределённые паттерны)
AI-агент для доменаскиллы ddd-tactical-review и ddd-tactical-design

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

  • В сервис добавляют 5-й, 6-й, 10-й внешний адаптер — каждый со своими SDK и квирками. Хочется их подменять в тестах.
  • Один и тот же UseCase нужно вызвать из REST, из Kafka-обработчика и из cron-задачи. Дублирование «адаптеров» становится проблемой.
  • Архитектурное правило «домен не зависит от Spring/jOOQ» постоянно нарушается, и хочется проверять его автоматически.

Тогда — Уровень 4: изолированная инфраструктура.

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