Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Monorepo против multirepo: выбор структуры

Monorepo или много репозиториев: честное сравнение для команды

Что реально меняется с monorepo: атомарные изменения и общие стандарты против скорости CI и прав доступа; критерии выбора, инструменты и миграция без остановки.

Прямой ответ

Выбор решают три вопроса, а не мода. Связность кода: изменения регулярно затрагивают несколько сервисов/пакетов вместе (общие схемы, контракты, библиотеки) — monorepo даёт атомарность (контракт + его потребители меняются в одном PR — версии API без гонки релизов); сервисы независимы и команды изолированы — multirepo дешевле. Размер команды и права: monorepo предполагает доверие и общие стандарты (все видят всё — сильные CODEOWNERS частично закрывают границы); внешние контрибьюторы/жёсткое разделение доступа — multirepo. Готовность платить за CI: monorepo требует умного пайплайна (запускать тесты только затронутых пакетов — affected-логика, кеши сборки — Nx/Bazel-класс или самописные скрипты по диффу — Git-производительность); «CI гоняет всё на каждый коммит» не масштабируется уже к 10 пакетам. Практический опыт: monorepo окупается для продуктовой команды 3–20 человек с общими библиотеками и фронтенд+бэкенд связкой; multirepo — для независимых продуктов/команд и открытой разработки; гибриды (monorepo продукта + отдельные репо переиспользуемых библиотек — публикацией) — частый устойчивый компромисс. Ошибки выбора симметричны и исправимы: monorepo→разъезд (история фильтруется по подкаталогам) и multirepo→сборка (subtree-слияния) — не приговоры, а проекты на неделю.

Monorepo: что получаешь и за что платишь

Выигрыши: атомарные изменения (рефакторинг общей библиотеки + правки всех потребителей — один PR, один ревью, один деплой-момент — в multirepo это N PR с версионной хореографией); единые стандарты (линтер/форматтер/хуки одни на всё — pre-commit — стиль кода унифицирован без переговоров между репо); переиспользование без публикации (общий пакет — workspace-зависимость из соседней директории — изменения видны мгновенно, без релиза версии и bump'ов); один контекст (поиск по кодовой базе, общие issue, одна точка входа для новичка). Издержки: CI-сложность (наивный «тестировать всё» умирает к 20 пакетам — нужна affected-логика: дифф → граф зависимостей → затронутые пакеты; кеширование сборок/тестов; Actions-матрица параллелит, но не заменяет граф); размер и клонирование (гигантский репо + история — partial clone/sparse-checkout — локальная производительность требует настройки — детали); размытие владения (без CODEOWNERS «чей код» размывается — ревью всего всеми не масштабируется); степень свободы конфликтов (общие файлы — точки пересечения параллельных веток — стратегии ветвления с короткими ветками почти обязательны).

Multirepo: независимость и её цена

Выигрыши: границы совпадают с командами (репо = сервис = владение = права доступа = пайплайн — организационная простота); лёгкий CI (репо маленькое — полный прогон минуты; кеши/графы не нужны); независимые релизы (сервис деплоится своим циклом; версии библиотек — явные контракты — SemVer-дисциплина обязательна и полезна); онбординг внешнего контрибьютора в один маленький репо проще. Издержки: версионная хореография (изменение общей библиотеки → релиз → bump у N потребителей → согласование деплоев — часы-дни вместо одного PR; зависимость «в разработке» живёт в git-ссылках/локальных путях — неудобство); сквозные изменения (смена формата события, затрагивающая 5 сервисов — 5 PR в 5 репо с риском рассинхрона деплоя — совместимость спасает, но процесс тяжелее); дублирование инфраструктуры (Dockerfile/CI-шаблоны/настройки линтера копируются — дрейфуют — лечение: шаблоны-репозитории и общие actions, что частично возвращает к «общему месту»); разрозненный поиск («где используется этот тип?» — по репозиториям, не по кодовой базе — code search инструменты или принятие боли).

Инструменты и миграция

Monorepo-стек по размеру: до ~10 пакетов — workspace'ы пакетных менеджеров (uv/pnpm/npm workspaces: локальные зависимости, единый lock, один install) + самописный affected-скрипт в CI (git diff --name-only → список пакетов → выборочный запуск — 30 строк bash покрывают 80% потребности); дальше — Nx/Turborepo/Bazel (граф зависимостей, инкрементальные сборки, удалённый кеш — окупаются на десятках пакетов и частых коммитах); Python-специфика — uv workspace/pyproject-иерархия + pytest-пути (структура проекта в масштабе репо). CODEOWNERS на подкаталоги — права ревью по владению; sparse-checkout — клонирование только своей части (для огромных репо). Миграция multirepo→monorepo: git subtree add/filter-repo с сохранением истории по пакетам (история — не «один initial commit» — blame и археология выживают), период двойной жизни (старые репо read-only с README-указателем), перенос CI (один пайплайн с affected вместо N простых — самый долгий этап). Обратная миграция — filter-repo по подкаталогу + перенос пайплайна — симметрично.

Практический сценарий: съезд четырёх репозиториев

Контекст: продуктовая команда 7 человек: API, админка-фронт, общий пакет схем, воркер — четыре репо; боль: изменение схемы события = 4 PR за два дня с версионным пинг-понгом, линтеры в репо разошлись, дубль CI-шаблонов. Съезд: (1) структура — packages/schemas, services/api, services/worker, apps/admin — uv workspace + pnpm workspace (два лок-файла — по экосистемам); (2) история — git filter-repo на каждом + subtree-слияние в новый репо (blame живой); (3) CI — affected-скрипт: дифф → затронутые пакеты → их тесты + зависимые (граф — из lock-файлов, 40 строк python); полный прогон — nightly и на main; (4) CODEOWNERS — по сервисам; переход двух недель с заморозкой фич (мигрировали инфраструктуру, не код). Итоги квартала: сквозные изменения — один PR (схема+потребители — часы вместо дней), стиль кода выровнялся (один ruff/prettier на всё), CI-время на типичный PR — 4 минуты (affected) против 12 (полный прогон был бы); издержки проявились в ожидаемом месте — общий lock-файл стал точкой конфликтов (решено короткими ветками и частым rebase — ветвление). Библиотеку, которую хотят внешние пользователи, через полгода вынесли обратно в отдельный репо с публикацией — гибрид оказался устойчивой точкой.

FAQ

Monorepo = монолит? Нет, это ортогонально: monorepo — про хранение кода, монолит — про архитектуру запуска; в monorepo живут микросервисы с независимыми деплоями (и наоборот — монолит можно разнести по репо); «monorepo затягивает в монолит» — риск связности, лечится границами пакетов и независимыми пайплайнами деплоя.

Какой размер репо начинает тормозить Git? Порядки: единицы ГБ и сотни тысяч файлов — заметны (клонирование, status) — лечатся partial clone/sparse-checkout/shallow-стратегиями; Google/Meta-масштабы — отдельная инженерия (не ваш случай); продуктовые monorepo до десятков тысяч файлов — комфортны без экзотики.

Что делать с общими настройками в multirepo? Репозиторий-шаблон (cookiecutter/copier-класс) + общий CI-actions/пакет конфигов версионируемой зависимостью (линтер-конфиг — пакет, не копипаста) — «единые стандарты без единого репо»; дрейф всё равно случается — ревизия шаблона плановая.

Нужен ли monorepo для микросервисов? Не обязателен и не противопоказан: микросервисы живут и в multirepo (классика), и в monorepo (общие схемы/контракты в одном PR — сильная сторона); решение по связности изменений, не по архитектурному стилю — сервисы с частыми совместными правками выигрывают от одного репо, полностью независимые — нет разницы.

Как выбрать, если команда спорит? Считать сквозные изменения: взять последние 20 задач и отметить, сколько затрагивало >1 сервиса — больше трети при общих контрактах — аргумент за monorepo; почти все в рамках сервиса — multirepo не мешает; решение фиксировать ADR'ом с критериями пересмотра (документирование решений) — через год контекст изменится, и решение пересматривается по данным, не по памяти о споре.

Источники: Nx mental model, Google «Why» (open source docs), git-scm — ссылки в шапке.