← назад к методологии · уровень 3 из 3 · предыдущий: уровень 2
Какую проблему решает
На Уровне 2 операции выделены, но бизнес-правила всё ещё живут в handler-ах, а инфраструктура (jOOQ, Spring, Kafka) перемешана с логикой. Два класса проблем:
- Размытые правила. Одно правило проверяется в трёх handler-ах; кто-то забывает — баг. Язык расходится («order» / «purchase» / «sale»). Логику «оплатить заказ» не отрефакторить — нет границ.
- Связанность с инфраструктурой. Репозиторий импортирует 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; сам о них не знает.
Границы проверяются автоматически в CI — HexagonalArchitectureRules.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), список входных/выходных портов и адаптеров. Раздел «Доменная модель» становится опорным.
Что почитать рядом
- Тактические паттерны DDD — Entity, Value Object, Aggregate, Domain Event, Repository.
- Стратегические паттерны DDD — Bounded Context, Context Map.
- Гексагональная архитектура — детальный разбор портов и адаптеров.
- Библиотека ddd-building-blocks — готовые абстракции.
- Распределённые паттерны — Outbox, Saga, Idempotent Consumer.
- Паттерны отказоустойчивости — что должно быть в адаптере выходного порта.