Уровень зрелости 4: Инфраструктура изолирована

Четвёртый уровень Use Case Pattern: бизнес-логика и инфраструктура разнесены через порты и адаптеры. Один UseCase вызывается из REST, очереди и cron без дублирования.

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

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

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

На третьем уровне у вас уже есть домен, агрегаты, события — бизнес-правила в порядке. Но инфраструктура всё ещё перемешана с логикой: репозиторий импортирует jOOQ, handler знает про Spring, контроллер тащит в UseCase HttpServletRequest. Когда инфраструктура меняется (новый платёжный шлюз, переезд на Kafka, замена БД) — приходится править много мест.

Четвёртый уровень делает простую вещь: бизнес-логика не должна знать, в каком окружении она работает. Между ней и инфраструктурой проводится жёсткая граница — порты и адаптеры.

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

  • Десятки внешних интеграций — платёжные шлюзы, логистика, антифрод, сертификаты, налоги. Каждая со своим SDK, квирками, периодическими отказами.
  • Один и тот же UseCase нужно вызывать из разных каналов: REST, обработчика очереди, cron, тестов — без дублирования.
  • Долгоживущий продукт (5+ лет), бизнес-логика стабильнее инфраструктуры.
  • Команда хочет менять адаптеры без страха уронить домен — например, мигрировать с Kafka на NATS, или с PostgreSQL на CockroachDB.
  • Тесты бизнес-логики должны быть быстрыми и не зависеть от Spring-контекста.

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

Жёсткое разделение пакетов. Корень проекта делится на два больших мира:

core/<bounded-context>/
    domain/        ← агрегаты, value objects, события (то, что было на уровне 3)
    usecase/       ← UseCase + Handler (бизнес-логика)
    port/          ← интерфейсы для внешнего мира — порты

adapter/
    in/rest/       ← REST-контроллеры
    in/kafka/      ← обработчики очередей
    in/scheduler/  ← cron-задачи
    out/postgres/  ← реализации репозиториев и портов на jOOQ
    out/payment/   ← обёртка над платёжным шлюзом
    out/...        ← другие внешние сервисы

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

Все внешние взаимодействия — через порты в core/<bc>/port/. Платёжный шлюз — PaymentGateway интерфейс в core, реализация в adapter/out/payment/. Шина событий — DomainEventPublisher (уже есть в ddd-building-blocks), реализация в adapter/out/kafka/. Хранилище — OrderRepository интерфейс в core, реализация в adapter/out/postgres/.

Один UseCase — несколько входных адаптеров. CreateOrderUseCase может вызываться из REST-контроллера, из обработчика Kafka-сообщения и из cron-задачи. Все три места — это входные адаптеры. Сам UseCase ничего о них не знает.

Тесты бизнес-логики — без Spring. Handler-тест: создал в памяти fake-реализации портов (или mock), вызвал handler.handle(useCase), проверил эффекты. Никакого @SpringBootTest, никакого Testcontainers, никакого 30-секундного старта.

Как проверять автоматически

Чтобы граница не «протекала», правила нужно закрепить в CI:

  • ArchUnit-тест: «классы из core.** не импортируют org.springframework.**, org.jooq.**, org.apache.kafka.**».
  • ArchUnit-тест: «реализации портов лежат только в adapter.out.**».
  • ArchUnit-тест: «контроллеры и обработчики очередей не вызывают друг друга — только UseCase через диспетчер».

Эти 3–5 правил отлавливают 90% попыток вернуть «удобный, но грязный» импорт.

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

Спека по-прежнему Tier C, но в разделе «Интеграции (Context Mapping)» добавляются:

  • список входных и выходных портов;
  • какие адаптеры реализуют каждый порт;
  • типы Context Mapping для пар: ACL, Open Host Service, Conformist, Customer/Supplier.

Стоимость

Уровень 4 не бесплатный. Он добавляет:

  • 2× больше пакетов — на каждый порт интерфейс + хотя бы одну реализацию.
  • Дополнительные ArchUnit-тесты в CI и культуру их поддерживать.
  • Дисциплина в обзорах — нельзя «срочно дописать sql-запрос прямо в handler-е, потом перенесём».

Если у проекта мало интеграций и горизонт жизни короткий — четвёртый уровень принесёт боли больше, чем пользы. Это не «продвинутый = лучший». Это «нужный, когда есть причина».

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

ЧтоЧем
UseCase + Handlerusecase-pattern-starter
Доменные абстракцииddd-building-blocks
Аннотации портов/адаптеров + ArchUnit-правилаhexagonal-architecture (@InboundPort / @OutboundPort / @InboundAdapter / @OutboundAdapter + готовый HexagonalArchitectureRules.verify(...))
Маппинг JsonBean ↔ доменMapStruct
Готовые рецепты для адаптеров отказоустойчивостиResilience4j
Проверка контрактов с соседямиOpenAPI / AsyncAPI + Pact для consumer-driven
AI-агентыскиллы ucp-pattern-review проверяют, что граница не протекла

Что дальше

Дальше идти некуда — это последний уровень. Но это не финал, потому что:

  • В одном сервисе разные модули могут жить на разных уровнях. Ядро бизнеса на 4-м, справочники — на 1-м. Это нормально.
  • Уровень 4 хорошо ложится на модульный монолит: те же core/ + adapter/ модули, упакованные в один деплой. Когда нагрузка вырастет — выделение микросервиса не требует переписывания, только сборка отдельного артефакта.

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