ADR-0001: Monorepo с модулями по Clean Architecture
Status: accepted Date: 2026-04-17 Deciders: команда проекта
Контекст
Требуется архитектура, позволяющая:
- легко добавлять новых поставщиков;
- разделять ответственность по модулям (catalog, offers, pricing, search, …);
- избежать распределённой сложности на раннем этапе;
- сохранять возможность выделить сервис в отдельный процесс позже.
Альтернативы: полноценные микросервисы с отдельными репозиториями vs monorepo с модульным монолитом vs монолит без чёткой модульной границы.
Решение
Monorepo + модульный монолит с Clean Architecture в каждом модуле.
- Один Git-репозиторий, несколько бинарников в
cmd/(api-server, ingestor, matcher, estimate-builder). - Бизнес-логика в
core/<module>/, коннекторы вconnectors/<supplier>/. - Каждый модуль следует раскладке
domain / app / infra / api. - Ядро не импортирует коннекторы; регистрация — в
cmd/*/main.go.
Последствия
Плюсы
- Единое версионирование, атомарные PR через несколько модулей.
- Простота shared кода (platform/*).
- Быстрая эволюция на ранней стадии.
- Низкая операционная сложность (меньше сервисов — меньше оперирования).
- Архитектурные правила проверяются линтером внутри репо.
Минусы
- Требуется дисциплина импортов — без линтера границы размоются.
- CI на все модули при каждом PR (mitigate: build matrix по affected paths).
- Размер репо со временем растёт.
Нейтральные последствия
- Всегда можно извлечь сервис в отдельный процесс (
cmd/), не меняя доменного кода.
Модель деплоя и хостинг коннекторов
Стартовая модель — один процесс на роль из cmd/: api-server, ingestor, matcher, estimate-builder, outbox-relay, supply-chain-recalculator. Все коннекторы регистрируются в одном бинаре cmd/ingestor/main.go и работают внутри одного процесса (in-process plugin модель). Это исходное состояние на Phase 0–2.
Коннекторы НЕ являются отдельными deployable сервисами. Коммуникация ядра с коннектором — обычный Go-вызов через интерфейс connector.Connector, без сетевого hop’а.
Когда выделять отдельный процесс/сервис
Триггеры для выделения (любой ≥ 1):
| Триггер | Порог |
|---|---|
| Изоляция сбоев | один коннектор регулярно роняет общий ingestor (OOM, panic, нативный код) |
| Rate-budget нагрузка | коннектор требует > 30% CPU процесса в установившемся режиме |
| Независимые scaling-профили | пиковая нагрузка коннектора в N раз отличается от среднего по группе |
| Compliance / data residency | коннектор должен жить в отдельном сетевом сегменте |
| Lifecycle | коннектор обновляется существенно чаще ядра (несовместимо с release-циклом) |
Процедура выделения
- Создаётся новый
cmd/ingestor-<supplier_id>/main.goс регистрацией только этого коннектора. - Контракт остаётся доменный (Go interface) — общение всё ещё через kafka topics + outbox + общий PG, без RPC между процессами.
- Деплой как отдельный сервис в k8s, общие
event_store/outbox/Kafka. - Ровно тот же доменный код — без переписывания.
Этот переход — без ADR (это не архитектурный сдвиг, это deployment-вариант, заранее предусмотренный). Уход от модульного монолита целиком (выделение core/* в микросервисы) — отдельный ADR.
Рассмотренные альтернативы
Микросервисы с отдельными репо
Преждевременная распределённая сложность, тяжёлое shared-ownership, медленная разработка. Не подходит для текущего масштаба команды.
Монолит без модульной границы
Быстро превращается в клубок. Отсутствие архитектурных барьеров — на опыте гарантированная боль через 6 месяцев.
Ссылки
- Principles:
../principles.md, P3 - Layout:
../module-layout.md