Опирается на правила:
R-HEX-MOD-1…R-HEX-MOD-5иR-HEX-MOD-X1…R-HEX-MOD-X3из Hexagonal Style Guide → раздел 2. Структура модулей.
Важно знать
- Hexagonal — это multi-module gradle, не «один модуль с папками».
core/— единственный модуль без Spring. Чистый Java + Lombok + DDD-библиотеки.- Per-purpose:
user-api-in-adapter,admin-api-in-adapter,kafka-in-adapter— каждый со своим security/конфигом.- Per-system out:
persistence/,sber-out-adapter/,kafka-out-adapter/— отдельный модуль на каждую внешнюю систему.bootstrap/— composition root:@SpringBootApplication+application.yml+ сборка всех адаптеров. Бизнес-логики не содержит.- Стрелка зависимостей строго:
bootstrap → core ← adapters.coreне зависит от адаптеров. Адаптеры не зависят друг от друга.
Архитектура — это не «как назвали пакеты», а что компилятор реально запрещает. Gradle-модуль — единственный механизм в Java, который даёт compile-time изоляцию: класс из модуля A не увидит класс из модуля B, если в build.gradle модуля A нет на B зависимости. Hexagonal без multi-module gradle — это конвенция, которую первый невнимательный коммит сломает. С multi-module — это compile-error. Раскрытие правил R-HEX-MOD-* ниже.
Многомодульный gradle-проект
R-HEX-MOD-1: типичная раскладка сервиса.
<service>/
├── core/ # Pure Java, без Spring
├── persistence/ # out-adapter для PG (jOOQ)
├── user-api-in-adapter/ # in-adapter REST (user-facing)
├── admin-api-in-adapter/ # in-adapter REST (admin)
├── kafka-in-adapter/ # in-adapter Kafka consumers (если есть)
├── kafka-out-adapter/ # out-adapter Kafka producers (если есть)
├── sber-out-adapter/ # out-adapter для Sber (или другая внешняя система)
├── sms-out-adapter/ # out-adapter для SMS-провайдера
├── scheduler-out-adapter/ # out-adapter для @Scheduled tasks
└── bootstrap/ # composition root: Spring Boot Application + конфиги
settings.gradle.kts включает все модули:
include(
":core",
":persistence",
":user-api-in-adapter",
":admin-api-in-adapter",
":kafka-in-adapter",
":kafka-out-adapter",
":sber-out-adapter",
":sms-out-adapter",
":scheduler-out-adapter",
":bootstrap"
)
Минимальный набор для Уровня 3 — core/, persistence/, один *-in-adapter, один *-out-adapter (если выходим за PG), bootstrap/. Дальше — добавляем модули по мере роста.
core/ — единственный без Spring
R-HEX-MOD-2: core/ зависит только от JDK, Lombok, DDD-библиотек, jakarta.validation API.
// core/build.gradle.kts
dependencies {
implementation("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
implementation("ru.vikulinva:ddd-building-blocks:1.x")
implementation("ru.vikulinva:usecase-pattern:1.x")
implementation("ru.vikulinva:hexagonal-architecture:1.x")
implementation("jakarta.validation:jakarta.validation-api")
// НЕТ spring-boot, jooq, jackson, okhttp, kafka-clients...
}
Что это даёт:
- Compile-time guarantee. Невозможно случайно положить
import org.springframework.*вcore/— gradle на компиляции скажет «класс не найден». Это и есть та граница, которую руками не удержать. - Быстрые unit-тесты. Тесты на агрегатах работают за миллисекунды, без
@SpringBootTest, без Testcontainers, без warmup. Можно запускать тысячами на каждый коммит. - Переиспользуемость. Тот же
core/можно завернуть в Lambda-handler, CLI-утилиту, batch-task — без переписывания. На практике мало кто переиспользует, но гарантия чистоты домена ценна сама по себе.
См. также Core слой — внутреннее устройство core/.
Per-system out-адаптеры
R-HEX-MOD-3: на каждую внешнюю систему — отдельный gradle-модуль.
persistence/ # PG через jOOQ
sber-out-adapter/ # Sber-API через RestClient
ok-out-adapter/ # OdnaKassa-API
sms-out-adapter/ # SMS-провайдер
kafka-out-adapter/ # Kafka producers (если есть direct send)
s3-out-adapter/ # S3 / object storage
scheduler-out-adapter/ # @Scheduled — это тоже out, дёргает port-методы
Зачем разделять:
- Изоляция dependency.
core/не зависит от Sber-SDK.persistence/не знает Kafka. Если завтра меняем провайдера SMS — правка в одномsms-out-adapter/, остальные модули не пересобираются. - Версионирование. Sber-SDK обновился до v3 — обновили один модуль, тестим, релизим. Не «обновили SDK во всём сервисе и теперь надо проверить весь функционал».
- Resilience-конфиг per-system. OkHttpClient, Resilience4j (Circuit Breaker, Bulkhead, Retry) настраиваются отдельно для каждой системы. Это правило
R-RES-ISO-1из Resilience Style Guide — per-system isolation. Один общий HttpClient для всего исходящего трафика — антипаттерн.
Per-purpose in-адаптеры
R-HEX-MOD-4: каждый тип входа — свой *-in-adapter.
user-api-in-adapter/ # REST для конечного пользователя
admin-api-in-adapter/ # REST для админ-панели
kafka-in-adapter/ # Kafka consumers как entry-point (если consumer != просто sync-read)
cli-in-adapter/ # CLI / batch — если есть
Зачем разделять:
- Разный security-профиль.
user-api-in-adapterпринимает JWT от Keycloak;admin-api-in-adapter— JWT с другим audience и mTLS. SecurityFilterChain в одном модуле = один профиль, без рисков перепутать. - Разный OpenAPI. User-API публикуется наружу для клиентских команд; admin-API — внутренний. Два отдельных swagger-файла, две отдельные
*Api-интерфейсные генерации. - Compile-time изоляция. Контроллер user-API не сможет случайно вызвать admin-внутренний-handler — он его не видит. Это защита от расширения user-API «через чёрный ход».
bootstrap/ — composition root
R-HEX-MOD-5: bootstrap/ собирает всё.
// bootstrap/build.gradle.kts
dependencies {
implementation(project(":core"))
implementation(project(":persistence"))
implementation(project(":user-api-in-adapter"))
implementation(project(":admin-api-in-adapter"))
implementation(project(":kafka-in-adapter"))
implementation(project(":sber-out-adapter"))
implementation(project(":sms-out-adapter"))
implementation(project(":scheduler-out-adapter"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-jooq")
// ...
}
Содержит:
<App>Application.javaс@SpringBootApplication.application.yml+ профили (application-local.yml,application-prod.yml).@Configuration-классы для wiring beans, которые не покрываются component scan'ом.Dockerfile, Helm chart.
Зависит от всех модулей. Никто не зависит от bootstrap/ — это закрывающий узел.
Никакой бизнес-логики, никаких контроллеров здесь нет. См. Bootstrap / composition root.
Стрелка зависимостей
Это самое важное правило, из которого следует всё остальное:
bootstrap → core ← adapters
bootstrapзависит от всех.- Каждый
adapterзависит только отcore(через port-интерфейсы). coreне зависит ни от чего инфраструктурного.- Адаптеры не зависят друг от друга. Координация — это use-case в
core/.
В gradle это выражается явно:
// persistence/build.gradle.kts
dependencies {
implementation(project(":core")) // ← только core
// НЕТ implementation(project(":sber-out-adapter")) ← запрещено
}
Если что-то нарушает стрелку — gradle упадёт на сборке. Это и есть compile-time гарантия Hexagonal'a, которую не даст никакая «папка с правильным именем».
Что запрещено
R-HEX-MOD-X1: один gradle-модуль для всего сервиса с папками core/, adapter/.
<service>/
└── src/main/java/
├── core/...
└── adapter/... # ← одна compile-зона
Это «архитектура на бумаге»: гарантий компилятора нет. Кто-то добавит import org.springframework.* в файл из пакета core/, и никто не заметит — IDE подсветит как валидный импорт, тест пройдёт, ревью пропустит. Через полгода core/ уже не чистый.
Multi-module gradle — единственный способ это обеспечить.
R-HEX-MOD-X2: core/ зависит от persistence/ или любого другого адаптера.
// core/build.gradle.kts
dependencies {
implementation(project(":persistence")) // ← НЕТ, стрелка вывернута
}
Это разрушает всю модель. core/ теперь тянет JOOQ, через JOOQ — HikariCP, через HikariCP — JDBC-драйвер. Чистого domain больше нет.
Стрелка всегда: bootstrap → core ← adapters. Если кажется, что core/ нужен persistence/ — это сигнал, что в core лежит то, чему там не место (например, generated POJO).
R-HEX-MOD-X3: все REST в одном *-in-adapter — user + admin endpoints вместе.
Compile-time изоляция security-моделей теряется. Один и тот же SecurityFilterChain обслуживает оба контракта, и любая ошибка в @PreAuthorize распространяется через оба. На admin-API случайно может попасть user-токен с правами админа — и проверки не покажут это до прода.
Два отдельных *-in-adapter, два отдельных @EnableWebSecurity-конфига. Дороже на 200 строк boilerplate, но дешевле, чем security-incident.
Куда дальше
- Hexagonal Style Guide → раздел 2. Структура модулей — нормативные формулировки.
- Core слой — внутреннее устройство
core/. - Ports — что лежит на границе
core↔ адаптеры. - Bootstrap / composition root — как
bootstrap/собирает всё. - Resilience Style Guide → R-RES-ISO-1 — про per-system isolation.