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 и whitelist platform/{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-lint v1.
  • Миграция существующего 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.

Ссылки