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-циклом)

Процедура выделения

  1. Создаётся новый cmd/ingestor-<supplier_id>/main.go с регистрацией только этого коннектора.
  2. Контракт остаётся доменный (Go interface) — общение всё ещё через kafka topics + outbox + общий PG, без RPC между процессами.
  3. Деплой как отдельный сервис в k8s, общие event_store/outbox/Kafka.
  4. Ровно тот же доменный код — без переписывания.

Этот переход — без ADR (это не архитектурный сдвиг, это deployment-вариант, заранее предусмотренный). Уход от модульного монолита целиком (выделение core/* в микросервисы) — отдельный ADR.

Рассмотренные альтернативы

Микросервисы с отдельными репо

Преждевременная распределённая сложность, тяжёлое shared-ownership, медленная разработка. Не подходит для текущего масштаба команды.

Монолит без модульной границы

Быстро превращается в клубок. Отсутствие архитектурных барьеров — на опыте гарантированная боль через 6 месяцев.

Ссылки