ADR-0031: Microkernel sub-modules per bounded context
Status: accepted Date: 2026-04-20 Deciders: команда проекта
Контекст
ADR-0001 зафиксировал монорепозиторий и модульный монолит с Clean Architecture в каждом BC на уровне четырёх слоёв (domain / app / infra / api). По мере роста внутри одного BC (пример: core/catalog с агрегатами unit, characteristic, manufacturer, canonical) все файлы каждого слоя скапливаются плашмя рядом друг с другом. Нет формальных границ между агрегатами внутри BC, нет отдельной локальной документации, и линтер не умеет отличить «своё» от «чужого» на уровне агрегата. При появлении других BC (offers, pricing, search, matching, meta-search, enrichment) такая раскладка становится помойкой.
Решение
Каждый bounded context = тонкое ядро + набор суб-модулей-«плагинов». Суб-модуль по умолчанию соответствует одному агрегату; допускается объединение нескольких мелких агрегатов в один суб-модуль, если они разделяют общий инвариант и никогда не используются по отдельности — с обоснованием в README суб-модуля.
Раскладка:
backend/internal/core/<bc>/
├── AGENTS.md
├── README.md
├── di.go # fx.Module() BC: собирает суб-модули
├── kernel/
│ ├── domain/ # BC-wide VO/ID/errors (используется ≥2 суб-модулями)
│ └── infra/ # shared pg helpers и т.п.
└── <submodule>/
├── AGENTS.md
├── README.md
├── di.go # fx.Module() суб-модуля
├── domain/ # плоский layout
├── app/
├── infra/
│ └── postgres/
└── api/
└── http/
Правила зависимостей (машинно проверяются):
domain/суб-модуля — чистый; импортирует толькоkernel/domainи whitelistplatform/{clock,errors,money,ids}.app/— импортируетdomain/этого же суб-модуля + те же платформенные пакеты. Ниinfra/, ниapi/, ниdomain/соседнего суб-модуля.infra/— импортируетdomain/этого суб-модуля +kernel/**+platform/**.api/— импортируетapp/этого суб-модуля +platform/httpx+platform/errors.- Между соседними суб-модулями прямые импорты запрещены. Взаимодействие через consumer-owned порт в
domain/потребителя + адаптер в егоinfra/+ проводка вcore/<bc>/di.go. - Между BC прямые импорты запрещены, как и раньше (ADR-0001 принцип сохранён).
Линтер: github.com/fe3dback/go-arch-lint@v1.14.0 (пин в .gitlab-ci.yml и Makefile). Конфиг backend/.go-arch-lint.yml генерируется из исходного дерева инструментом backend/cmd/archlint-gen, чтобы обойти glob-merging и иметь явные per-submodule компоненты (иначе import canonical/domain → manufacturer/domain не ловится). Дрейф конфигурации ловится в CI через git diff --exit-code.
CI: отдельный job backend-archlint в существующем stage verify пайплайна GitLab, рядом с backend-errorlint и backend-test.
Документация: каждый суб-модуль обязан содержать AGENTS.md и README.md по шаблонам из docs/docs/20-architecture/templates/. Шаблоны не шаблонизатор — просто cp при заведении нового суб-модуля.
Последствия
Плюсы
- Явные границы владения: 1 агрегат = 1 каталог = 1 владелец.
- Масштабируется при росте BC — новые агрегаты не засоряют соседей.
- Линтер делает правила выполнимыми, а не мыслительными.
- Локальные
AGENTS.mdфокусируют как агентов, так и людей, на контексте того, что именно они правят.
Минусы
- Каталогов становится больше; навигация по IDE чуть дороже.
- Требуется генератор
.go-arch-lint.ymlиз-за glob-merging вgo-arch-lintv1. - Миграция существующего BC (
catalog) — отдельная работа.
Нейтральные последствия
- Суб-модуль остаётся Go-пакетом; никаких новых go-модулей не вводится.
- При выделении сервиса в отдельный процесс (сценарий ADR-0001) суб-модули всё ещё лежат внутри одного BC и не требуют дополнительной подрезки.
Отношение к ADR-0001
ADR-0001 остаётся актуальным для внешней раскладки (monorepo, cmd/*, core/<bc>, connectors/*, platform/*) и стратегии deployment. Пункт ADR-0001 об intra-BC layout (domain / app / infra / api как единственный уровень разбиения) уточняется и в этой части заменяется настоящим ADR. Тело ADR-0001 не редактируется (ADR иммутабельны); актуальное описание раскладки BC — здесь и в module-layout.md.
Рассмотренные альтернативы
Оставить flat-layer внутри BC
Уже на catalog видно, как layer-каталоги превращаются в свалку per-aggregate файлов. При росте до 7+ BC деградация гарантирована.
Horizontal-by-layer с aggregate-subdirs (domain/<aggr>/, app/command/<aggr>/, …)
Это предыдущая черновая «таргет-раскладка» из module-layout.md. Решает часть проблемы группировки, но не даёт локального AGENTS.md/README.md на агрегат и не создаёт естественной границы изоляции — импорты между агрегатами в одном app/command/ пакете всё ещё свободны.
Выделять каждый агрегат в отдельный go-модуль
Излишняя сложность на текущей стадии; теряет атомарность PR в рамках BC; противоречит ADR-0001.
Ссылки
- ADR-0001 (monorepo + clean arch):
0001-monorepo-with-clean-architecture-modules.md - ADR-0030 (uber-fx DI):
0030-runtime-di-uber-fx.md - Living docs:
../module-layout.md,../principles.md - Templates:
../templates/ - Tool:
backend/cmd/archlint-gen, configbackend/.go-arch-lint.yml - Spec:
docs/superpowers/specs/2026-04-20-backend-microkernel-submodules-design.md