Опирается на правила:
R-HEX-WHEN-1…R-HEX-WHEN-3иR-HEX-WHEN-X1…R-HEX-WHEN-X2из Hexagonal Style Guide → раздел 1. Когда переходить на Hexagonal.
Важно знать
- Hexagonal — часть Уровня 3 в UCP. На Уровне 1–2 — overkill, ceremony без выгоды.
- Признаки «пора»: 2+ внешних системы, богатая domain-логика, 3+ способа входа, команда 3+ человек, тесты требуют полконтекста Spring.
- Признаки «рано»: один сервис с одной PG, 1–2 разработчика, < 10K LOC, бизнес-логика ещё ищет форму.
- Cargo-cult запрещён — Hexagonal не для всех сервисов. Решение про переход — про экономику, не моду.
- Частичный Hexagonal — антипаттерн. Либо полный (
core/persistence/*-in-adapter/*-out-adapter/bootstrap), либо ничего. Гибрид теряет compile-time гарантии и оставляет ceremony без выгоды.
Hexagonal Architecture даёт сильные гарантии: чистый core/ без Spring, изоляция portов от инфраструктуры, ArchUnit-тесты, держащие границы. Но платит за это сложностью multi-module gradle, mapper'ами между слоями, ceremony при добавлении нового адаптера. Эту цену стоит платить только когда выгода реально проявляется. Раскрытие правил R-HEX-WHEN-* ниже.
Hexagonal — часть Уровня 3
R-HEX-WHEN-1: Hexagonal в UCP — часть Уровня 3 (DDD + Hexagonal). На Уровнях 1–2 — нет.
Краткая шкала Use Case Pattern по уровням зрелости:
- Уровень 1 — UseCase + Handler + Controller в одном модуле, без DDD-агрегатов, без явного hex-разделения. CRUD-сервисы и проекты на старте.
- Уровень 2 — UseCase + DDD-агрегаты (rich domain), но всё ещё в одном-двух модулях. Бизнес-логика выделена в domain, но «корки» core/adapter ещё нет.
- Уровень 3 — DDD + Hexagonal. Multi-module gradle, port-интерфейсы в core, adapter'ы реализуют их, ArchUnit-тесты. Это то, про что эта статья.
См. R-LAY-* в Use Case Pattern Style Guide — формальное определение уровней.
Применять Hexagonal на Уровне 1 — это как ставить разделение фронт/бек в одностраничном лендинге. Можно, но ceremony перевесит.
Признаки «пора»
R-HEX-WHEN-2: вот сигналы, при которых Hexagonal начинает давать измеримый выигрыш.
1. Сервис интегрируется с 2+ внешними системами. Типичный сетап Уровня 3 — PG + платежи + Kafka + SMS. У каждой внешней системы свой DTO, свои retry/timeout-настройки, свои failure-режимы. Без явных out-adapter'ов всё это смешивается в одном Service-классе, который растёт неуправляемо.
2. Доменная логика становится сложной. Появляются агрегаты с инвариантами (Order.confirm() проверяет 5 условий и эмитит 3 события), бизнес-правила перетекают между сценариями. Domain хочется изолировать от инфраструктуры — менять бизнес-правила, не трогая Spring-конфиги.
3. 3+ способа input. REST для пользователя + REST для админа + scheduled tasks + Kafka consumers. Каждый — отдельная entry-point с своей security-моделью. Без *-in-adapter/ модулей они начинают пересекаться по @Configuration и SecurityFilterChain.
4. Тесты требуют полконтекста Spring. Если для unit-теста бизнес-логики надо поднять @SpringBootTest, потому что domain-сервис зависит от Spring-аннотированных классов — Hexagonal с чистым core/ решит это. Pure Java unit-тесты на агрегатах работают за миллисекунды.
5. Команда 3+ разработчиков. Когда несколько человек редактируют один сервис, архитектурные границы становятся социальной потребностью. ArchUnit-тест ловит «новый коллега положил Spring-импорт в core», когда code review пропустит. На команде из 1–2 человек договорённости работают и без тестов.
Если хотя бы 3 из 5 пунктов — переходим на Hexagonal.
Признаки «рано»
R-HEX-WHEN-3: и наоборот — когда переход даст больше боли, чем пользы.
Один сервис с одной PG. Если внешний мир — только PG (через Liquibase + jOOQ), Hexagonal не нужен. Repository-pattern в одном модуле справится. См. Repository pattern в jOOQ — это базовая граница domain ↔ persistence, не требующая полного hex.
1–2 разработчика, < 10K LOC. Команда мала, конвенции держатся устно. Архитектурные тесты ради «как бы кто-нибудь не положил Spring в core» — overhead, когда core — три файла.
Активно меняется бизнес-логика, форма ещё не устаканилась. В стартапах первые 6–12 месяцев бизнес-модель скачет. Hex-структура с её mapper-ами и port-интерфейсами тормозит итерации: каждое «давай попробуем по-другому» = переписывание трёх mapper'ов и трёх портов. Сначала найти устойчивую форму, потом — Hexagonal.
Нет агрегатов с инвариантами. Если domain — это «таблица с CRUD-методами», hexagonal-разделение ничего не защищает. Anemic domain в hex-обёртке — самый дорогой вариант anemic-domain.
Cargo-cult и частичный hex — запрет
R-HEX-WHEN-X1: Hexagonal как cargo-cult — все сервисы команды причёсаны под один шаблон независимо от их сложности. Сервис Уровня 1 из 3 endpoints в hex-раскладке = 5 gradle-модулей, 8 mapper'ов, ArchUnit-тесты — ради чего? Это форма «архитектура ради архитектуры», против которой UCP в целом.
Решение принимается на сервис, не на команду. Один и тот же отдел может иметь сервис Уровня 1 (рядом с CRUD-витриной) и сервис Уровня 3 (рядом с биллингом).
R-HEX-WHEN-X2: Частичный Hexagonal — core/ есть, но *-in-adapter/ смешан с REST-контроллерами + бизнес-логикой. Или есть persistence/, но нет <system>-out-adapter/ для платежей, и Sber-вызовы лежат прямо в handler'е.
Что в этом плохого:
- Compile-time гарантии не работают. Если хотя бы один модуль смешивает слои, ArchUnit ловит часть нарушений, а другую — нет. Команда теряет уверенность «у нас всё чисто».
- Mental overhead остаётся. Читая сервис, разработчик каждый раз думает «а тут уже hex или ещё нет». Хуже, чем монолит.
- Migration debt накапливается. «Доделать hex когда-нибудь» обычно не делается — рефакторинг занимает 2–4 недели и плохо помещается в спринт.
Правило: либо полный hex (со всеми модулями, ArchUnit-тестами в CI, mapper'ами), либо никакого. Промежуточные состояния — допустимы только как короткий миграционный период с явным дедлайном.
Куда дальше
- Hexagonal Style Guide → раздел 1 — нормативные формулировки.
- Use Case Pattern Style Guide → R-LAY-* — формальное определение Уровней 1/2/3.
- Структура модулей — что именно строим, когда решили переходить.
- Core слой — что попадает в
core/и почему.