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

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

На Уровне 2 операции выделены, но бизнес-правила всё ещё живут в handler-ах, а инфраструктура (jOOQ, Spring, Kafka) перемешана с логикой. Два класса проблем:

  1. Размытые правила. Одно правило проверяется в трёх handler-ах; кто-то забывает — баг. Язык расходится («order» / «purchase» / «sale»). Логику «оплатить заказ» не отрефакторить — нет границ.
  2. Связанность с инфраструктурой. Репозиторий импортирует jOOQ, handler знает про Spring, контроллер тащит HttpServletRequest в UseCase. Смена платёжного шлюза или БД правит десятки мест.

Уровень 3 решает оба сразу и потому объединяет DDD (явный доменный слой) и Hexagonal (изоляция инфраструктуры портами). Это один уровень зрелости: выделять домен есть смысл там же, где проводить границу с инфраструктурой — сервисы со сложными инвариантами и долгим горизонтом жизни.

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

  • Сложные инварианты, которые нельзя нарушать никогда (отрицательный остаток, платёж в неподтверждённом заказе).
  • Доменный язык важен — термины бизнеса перетекают в код, ясно выделяются Bounded Context-ы.
  • Десятки внешних интеграций (платежи, логистика, антифрод), которые хочется подменять в тестах.
  • Один UseCase вызывается из REST, Kafka и cron без дублирования.
  • Долгоживущий продукт (5+ лет); бизнес-логика стабильнее инфраструктуры.

Доменный слой (DDD)

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

Entity — сущности с идентичностью внутри агрегата (позиция заказа): меняется только через корень.

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

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

Repository — доступ к агрегатам целиком: интерфейс в домене, реализация в инфраструктуре.

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

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

Изоляция инфраструктуры (Hexagonal)

Жёсткое разделение пакетов — два мира:

core/<bounded-context>/
    domain/        ← агрегаты, value objects, события
    usecase/       ← UseCase + Handler (бизнес-логика)
    port/          ← интерфейсы для внешнего мира
adapter/
    in/rest/  in/kafka/  in/scheduler/    ← входные адаптеры
    out/postgres/  out/payment/  out/...  ← реализации портов

Зависимости направлены строго внутрь. core/ импортирует только Java-стандарт + методологические библиотеки (usecase-pattern, ddd-building-blocks, hexagonal-architecture-core). Никаких org.springframework.web.*, org.jooq.*, org.apache.kafka.* в core.

Все внешние взаимодействия — через порты в core/<bc>/port/. Платёжный шлюз — интерфейс PaymentGateway в core, реализация в adapter/out/payment/.

Один UseCase — несколько входных адаптеров. CreateOrderUseCase вызывается из REST, Kafka и cron; сам о них не знает.

Границы проверяются автоматически в CIHexagonalArchitectureRules.verify(...) (ArchUnit): «core не импортирует Spring/jOOQ/Kafka», «реализации портов — только в adapter.out». 3–5 правил отлавливают 90% попыток вернуть грязный импорт.

Стоимость

Уровень 3 не бесплатный: ~2× пакетов (порт + реализация), ArchUnit-тесты в CI, дисциплина в обзорах («нельзя дописать SQL прямо в handler»). Если интеграций мало и горизонт короткий — он принесёт боли больше пользы. Это «нужный, когда есть причина», а не «продвинутый = лучший». В одном сервисе разные модули могут жить на разных уровнях: ядро бизнеса — на 3-м, справочники — на 1-м.

Что фиксируется в спеке

На этом уровне Use Case спецификация — Уровень 3, полной глубины: агрегаты и инварианты, value objects, доменные события (что публикуется и кто слушает), Bounded Context-ы и связи (ACL, OHS, Conformist), список входных/выходных портов и адаптеров. Раздел «Доменная модель» становится опорным.

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