Уровень зрелости 3: Доменный слой выделен
Третий уровень Use Case Pattern: бизнес-логика и инварианты живут в доменной модели — агрегаты, value objects, доменные события. Handler становится оркестратором.
← назад к методологии · уровень 3 из 4 · предыдущий: уровень 2
Какую проблему решает
На втором уровне команды и запросы разнесены, но бизнес-правила всё ещё живут в handler-ах. Когда правил становится много, начинаются три беды:
- Дублирование проверок. Одно и то же правило проверяется в трёх handler-ах, потому что три операции его затрагивают. Кто-то забывает — баг в проде.
- Размытый язык. Один разработчик пишет «order», другой «purchase», третий «sale» — про одно и то же. Бизнес-аналитик в шоке.
- Невозможность отрефакторить. Логика «оплатить заказ» собрана из 8 строк проверок и 4 вызовов репозиториев в
PayOrderHandler. Никаких границ — каждое изменение рискует уронить три других сценария.
Третий уровень добавляет доменный слой: явные понятия (Order, Customer, Money), их состояния, их инварианты и события, которые они порождают. Бизнес-логика живёт в домене, а не в handler-ах.
Когда подходит
- Сложные бизнес-инварианты, которые нельзя нарушать никогда (отрицательный остаток, платёж в неподтверждённом заказе).
- Доменный язык важен — команда работает с бизнесом плотно, термины перетекают в код.
- Ясно выделяются Bounded Context-ы: «оформление заказа» и «расчёт с продавцами» — это разные миры со своими правилами.
- Сервис будет жить и развиваться годами, нужна устойчивость к изменениям бизнес-правил.
Что должно быть
Aggregate Root — корень бизнес-объекта.
Один корень держит все инварианты внутри: Order отвечает за то, что сумма заказа сходится с суммой позиций, что нельзя оплатить отменённый заказ, что нельзя добавить позицию после отправки. Все изменения проходят через методы корня — никаких прямых сеттеров наружу.
Entity — сущности с идентичностью внутри агрегата. Например, позиция заказа — это Entity внутри агрегата Order. Имеет id, может меняться, но только через корень.
Value Object — типы без идентичности, описывающие значение.
Money, Email, Quantity, OrderId. Immutable, сравниваются по значению. Они снимают «примитивную одержимость»: вместо BigDecimal amount — Money 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 |
| Каркас UseCase | usecase-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: изолированная инфраструктура.
Что почитать рядом
- Тактические паттерны DDD — Entity, Value Object, Aggregate, Domain Event, Repository, Specification.
- Стратегические паттерны DDD — Bounded Context, Context Map.
- Библиотека ddd-building-blocks — готовые абстракции.
- Распределённые паттерны — Outbox, Idempotent Consumer, Saga.