06 апр. 2026 г.·6 мин чтения

Monorepo против multirepo: релизы, CI и владение кодом

Monorepo против multirepo влияет на релизы, затраты CI и владение кодом. Используйте это сравнение, чтобы решить, когда общие библиотеки стоит держать вместе.

Monorepo против multirepo: релизы, CI и владение кодом

Почему этот выбор становится дорогим позже

Сначала структура репозиториев кажется мелким организационным решением. Через несколько месяцев она уже формирует скорость релизов, обзоры и исправления багов.

Боль обычно начинается вокруг общих библиотек. Небольшое изменение в общем пакете может заставить несколько команд остановиться, повторно протестировать и синхронизировать релизы. То, что выглядело как быстрый фикс, превращается в согласования, пересборки и тайминг‑проблемы.

Один репозиторий часто кажется отличной идеей в начале. Вы можете обновить библиотеку и все приложения, которые её используют, в одном pull request. Нет публикации пакетов, нет гонки версий. Для небольшой команды, которая движется быстро, это реальное удобство.

Позже та же настройка может тянуть вниз. Больше людей трогает одну кодовую базу, очереди ревью растут, и одно слияние запускает CI‑задачи, которые нужны были только одной команде. Несвязанные изменения начинают попадать вместе, поэтому каждый релиз несёт больше рисков, чем должен.

Многие репозитории снимают другой тип боли. Команды реже мешают друг другу. Проще разделять права доступа. Каждая кодовая база двигается в своём темпе. Цена — постоянная административная работа: версионирование, заметки к релизам, обновления зависимостей, публикация, откаты и отдельные правила CI. По‑отдельности ни одна из этих задач не выглядит большой. Вместе они могут съедать часы каждую неделю.

Поэтому этот выбор — не вопрос стиля. Это компромисс между совместной скоростью сейчас и затратами на координацию позже. Игнорируйте его — и форма репозитория начнёт принимать решения за вас.

Этот паттерн часто встречается в растущих стартапах. Один репозиторий держит всё просто, пока три‑четыре команды не зависят от одной и той же сборки и тестового пути. Тогда изменение в общем коде перестаёт быть локальным: все ждут одно и то же конвейера.

Дорогая часть — не сам репозиторий. Дорогая часть — привычка релизов, которую он создаёт.

Что меняет единый репозиторий

Один репозиторий делает согласованные изменения простыми. Если веб‑приложению нужно новое поле, и общая библиотека валидации тоже нужно изменить, одна ветка может нести оба правки. Не нужно сначала публиковать пакет или гадать, какую версию безопасно подтянуть.

Он также даёт командам одно место для просмотра общего кода. Текущий API, тесты и примеры живут вместе. Для небольшой продуктовой команды эта ясность убирает много повседневного шума.

Недостаток быстро проявляется в CI. Небольшая правка в общей папке может разбудить большой набор тестов. Одна строчка в утилите может запустить сборки для веба, бэкенда, админки и внутренних инструментов. Хорошее кэширование и умные правила пайплайна помогают, но не меняют базовый компромисс. Один репозиторий может превратить дешёвую правку в дорогой пайплайн.

Владение тоже может размыться. Когда все могут править общий код, в конце концов все начинают это делать. Это кажется эффективным, пока никто не чувствует ответственности за стандарты ревью, обратимые изменения или чистку. Общие библиотеки без ясных владельцев обычно собирают быстрые фиксы и неловкие абстракции, которые остаются слишком долго.

Представьте команду, которая обновляет экраны биллинга и одновременно меняет общий пакет форматирования денег. В одном репозитории они могут выпустить обе правки вместе и сохранить работоспособность приложения. Но если этот пакет затрагивает несколько сервисов, тот же pull request может привлечь дополнительных ревьюверов и загрузить очередь CI.

Один репозиторий лучше работает, когда командам часто нужны согласованные изменения, общий код меняется быстро, и у каждой общей области есть ясный владелец. Без этих правил удобство быстро исчезает.

Что меняют многие репозитории

Много репозиториев даёт командам больше свободы. Один сервис может выпустить сегодня, другой — на следующей неделе. Команда платежей не ждёт релиза админки, мобильная команда остаётся на своих дедлайнах.

CI обычно становится легче. Небольшая правка в одном репозитории обычно означает меньший набор тестов, короткую сборку и меньше шума для тех, кому изменение не нужно. Когда продукт растёт и части живут в разных ритмах релизов, это экономит реальные деньги и время ожидания.

Цена возникает, когда код разделён между репозиториями. Общие библиотеки не обновляются сами по себе. Кто‑то должен выпустить новую версию, написать заметку, опубликовать и проследить, чтобы каждый репозиторий подтянул её. Эта работа управляемая один раз. Она утомительна, когда происходит каждую неделю.

Кросс‑репозиторные изменения замедляются специфическим образом. В одном репозитории один pull request может обновить API, общий пакет и приложение, которое от него зависит. В нескольких репозиториях тот же фикс может потребовать нескольких pull request'ов, нескольких CI‑прогонов и аккуратного порядка релизов. Если один репозиторий отстаёт, всё изменение остаётся наполовину сделанным.

Вы почувствуете дополнительную координацию, когда общие библиотеки часто меняются, несколько команд трогают один и тот же рабочий поток, одно обратимое изменение требует апдейтов во многих приложениях и никто явно не владеет версионированием. Много репозиториев подходит, когда команды действительно отделены, общий код меняется редко, и у каждого репозитория чёткие границы версий.

Практический тест — посмотреть на последние три продуктовых изменения. Если большинство из них оставалось внутри одной области, много репозиториев может подойти. Если каждое изменение перепрыгивало между сервисами, пакетами и приложениями, аккуратный разрыв на бумаге может создать больше админской работы, чем он убирает.

Как проявляется связка релизов

Связка релизов начинается, когда отдельные кодовые базы перестают двигаться в своём ритме. Часто это начинается с мелочи: общий тип, общий клиент или пакет, который импортирует каждое приложение. Код выглядит аккуратно, а план релизов становится сложнее.

Общие типы и интерфейсы создают тихие связи между командами. Добавьте одно обязательное поле в объект пользователя — и веб, админка и фоновые воркеры могут потребовать изменений, прежде чем кто‑то сможет выпустить с уверенностью. Обновление одной библиотеки превращается в несколько согласованных апдейтов.

Исправления багов дают тот же эффект. Общая auth‑библиотека может содержать серьёзный баг. Исправление займёт час, но выкатывание — неделю, если четыре команды должны подтянуть новую версию, прогнать тесты и вписать апдейт в свой спринт. Исправление есть, но продукт не двигается, пока не догонит самая медленная команда.

Зафиксированные версии усугубляют проблему со временем. Они защищают команды от неожиданных поломок — и это справедливо. Но они ставят команды на разные таймлайны. Одно приложение остаётся на 2.3 из‑за завтрашнего запуска, другое прыгает на 2.5 ради нужной фичи, третье пропускает два релиза и надеется обновиться позже. Скоро никто не говорит «общая библиотека» как об одной вещи — говорят, какую версию кто использует.

Чтобы заметить релизную связь, задайте прямые вопросы:

  • Какая команда вынуждена ждать?
  • Кто выполняет работу по обновлению?
  • Может ли одна команда выпустить в одиночку?
  • Сколько приложений нужно повторно протестировать?
  • Кто решает, когда старые версии перестают поддерживаться?

Если одно изменение библиотеки заставляет несколько команд остановиться и согласовать действия, у вас есть релизная связь, даже если репозитории отдельные. Количество репозиториев не убирает зависимость — меняется только место появления боли.

Вот почему владение важно не меньше инструментов. Если никто не владеет миграциями, старые версии накапливаются. Если одна команда владеет общим кодом, а другие зависят от него каждую неделю, эти команды могут блокироваться решениями, которые они не принимали. Такое напряжение — первый признак, что разделение обошлось дороже, чем дало выгоды.

Как растут расходы на CI в реальной жизни

Аудит сборочного пайплайна
Найдите, где расходуется время CI и инженерное время по приложениям, сервисам и общим пакетам.

Затраты CI редко взрываются в первый день. Они подкрадываются по мере роста продукта, расширения тестовой базы и увеличения числа пушей.

В монорепозитории крошечные правки могут разбудить огромный пайплайн. Небольшая правка в общей библиотеке может запустить сборки, тесты и превью по всему продукту, даже если большая часть кода не поменялась. Если правила по путям слабые или нет логики «только затронутые тесты», минуты раннера исчезают быстро.

Мульти‑репозиторий даёт другой счёт. Каждый репозиторий часто получает свой файл пайплайна, базовый образ, секреты, раннеры, правила линтинга и проверки безопасности. Это выглядит аккуратно, но настройки повторяются. Пять маленьких репозиториев могут обойтись дороже, чем один настроенный, если каждый устанавливает одни и те же инструменты и запускает одни и те же сканы.

Кэширование меняет расчёт. Так же как и выборочные тесты. Если CI повторно использует зависимости, пропускает неизменённые пакеты и запускает только тесты, затронутые изменением, один репозиторий может оставаться относительно дешёвым. Если каждая задача начинает с нуля, даже скромная база кода будет медленной и дорогой.

Большинство команд недооценивают человеческую стоимость. Облачные расходы видны. Время инженеров — нет. Ожидание чеков, исправление флейки, обновление скопированной CI‑конфигурации и пересборка одного и того же артефакта в нескольких репозиториях — всё это считается. Медленные пайплайны меняют поведение: люди пакуют большие изменения, ревьюверы дольше возвращают обратную связь, и баги путешествуют дальше прежде, чем их поймают.

Тут выбор становится практичным. Один репозиторий может тратить деньги на слишком широкие прогонки. Много репозиториев — на повторяющуюся настройку. Команды принимают лучшие решения, когда измеряют оба показателя: счёт за облако и часы, потерянные ожиданием. Если вы смотрите только на инвойс, вы пропускаете большую часть стоимости.

Кто должен владеть общим кодом

Общие библиотеки стареют плохо, когда править ими может кто угодно, а никто не отвечает за последствия. Структура репозиториев это не решит. Один репозиторий всё ещё может скрывать неясное владение, а много репозиториев будет работать хорошо, если люди знают, кто принимает решения.

Дайте каждой общей библиотеке одну явно названную группу владельцев. Держите её маленькой. Два‑три поддерживающих обычно лучше, чем комитет. Они должны решать, какие изменения принимать, когда выпускать релиз и как долго поддерживать старое поведение.

Лучшие владельцы обычно ближе к тем командам, которые используют библиотеку каждую неделю. Если платформенная команда владеет кодом, от которого зависят продуктовые команды, у продуктовых команд должен быть реальный голос. Дистанция ведёт к плохим решениям: поддерживающие перестают ощущать боль от ломания, а пользователи начинают обходить библиотеку вместо того, чтобы её улучшать.

Простое разделение работает хорошо: команда с глубочайшим знанием владеет внутренностями, а команды с наибольшим использованием помогают формировать публичный интерфейс.

Установите правила ревью рано, прежде чем маленькие изменения превратятся в длинную очередь и неожиданное ломание станет нормой. Держите правила лёгкими:

  • Владельцы библиотек утверждают изменения публичного API.
  • Как минимум одна команда‑потребитель ревьюит ломающее или рискованное изменение.
  • Каждый релиз получает короткую заметку об изменениях, а при необходимости — заметку по миграции.
  • Тесты покрывают поведение, на которое опираются другие команды.

Сюрпризы — то, что люди запоминают. Если обновление библиотеки тихо меняет возвращаемое значение, правило именования или формат конфигурации, другие команды теряют время на поиск проблемы. Обычно лучше сначала объявить депрекейшн, оставить старый путь работающим короткое время и удалить его в известную дату.

Небольшой пример делает это конкретным. Если checkout, billing и reporting используют одну и ту же библиотеку цен, billing может владеть логикой, но checkout и reporting должны ревьюить любые изменения публичных методов, которые они вызывают. Это ловит ломание рано и держит библиотеку полезной, а не пугающей.

Как решать шаг за шагом

Упростите версионирование
Установите правила версионирования и заметки по миграции, которые команда сможет поддерживать каждую неделю.

Начните с того кода, который вы уже разделяете, а не с идеологий репозиториев. Команды часто превращают это в теоретические споры, затем делят слишком рано или остаются вместе слишком долго.

Лучше использовать скучный подход — и поэтому он работает: создайте карту существующих зависимостей, наблюдайте за тем, что меняется, и измеряйте работу, которую команда платит каждую неделю.

  1. Перечислите каждый продукт, сервис и приложение, которые используют один и тот же код. Включите внутренние инструменты, если они зависят от тех же библиотек. Вам нужна простая карта реальных зависимостей, не догадок.
  2. Отметьте библиотеки, которые меняются постоянно. Пакет, который движется каждую неделю, создаёт ежедневную координационную работу. Библиотека, которая почти не меняется, слабее причина для разделения.
  3. Проверьте расписание релизов по командам, а не по архитектурной диаграмме. Если одна команда выпускает дважды в день, а другая — раз в месяц, тесная связь быстро становится дорогой.
  4. Сравните расходы CI с затратами на управление пакетами и дополнительные ревью. Один репозиторий может заставлять каждый pull request запускать слишком много. Многие репозитории уменьшают часть этого, но добавляют повышение версий, обновления зависимостей и дополнительные шаги релиза. Считайте часы, а не только минуты сборки.
  5. Протестируйте небольшое разделение прежде чем реорганизовывать всё. Выберите одну общую библиотеку с чёткими владельцами и границами. Перенесите только её, затем наблюдайте за скоростью релизов, сломанными сборками, задержками ревью и вопросами поддержки несколько недель.

Это работает лучше, чем большой рефакторинг, потому что даёт данные. Oleg Sotnikov часто использует такой подход в работе Fractional CTO: начать с одного ограниченного изменения, измерить эффект и затем решить, нужна ли более широкая перестройка структуры репозиториев.

Если пилотное разделение создаёт больше работы по версионированию, чем экономит, оставьте репозиторий в одном куске ещё на время. Если оно уменьшает блокировки релизов и режет шум в ревью — у вас есть веская причина продолжать.

Простой пример

Разблокируйте кросс-командные изменения
Спроектируйте границы релизов так, чтобы команды могли выпускать без ожидания чужой работы.

Представьте небольшую SaaS‑команду с клиентским веб‑приложением, админкой для поддержки и фоновым воркером, который отправляет биллинговые письма и синхронизации. Они также поддерживают две общие библиотеки: одну для правил авторизации и одну для UI‑компонентов.

В начале один репозиторий кажется отличным выбором. Разработчик может изменить библиотеку авторизации, обновить экран входа, поправить админку и настроить воркер в одной ветке. Одно ревью покрывает всё изменение. Один запуск CI показывает, не сломал ли рефактор что‑то. Для молодого продукта такая скорость трудно переоценить.

Именно поэтому монорепозиторий часто кажется лёгким выбором в начале. Если одни и те же люди владеют всем и обычно выпускаются вместе, один репозиторий убирает трения.

Давление появляется позже. Появляется команда партнёрского API с собственным ритмом релизов. Она не может ждать веб‑приложение и админку каждый раз, когда хочет выпустить маленькое изменение авторизации. Теперь связка релизов начинает мешать. Обновление библиотеки перестаёт быть внутренним рефактором — это решение о версиях.

Владение тоже становится сложнее. Оригинальная продуктовая команда всё ещё хочет двигаться быстро, а команда партнёрского API требует более строгих правил. Кто утверждает изменения в библиотеке авторизации? Кто решает, разрешено ли ломающее изменение? В одном репозитории команды часто решают это через жёстче прописанные правила владения и более узкие проверки CI. В нескольких репозиториях это решают правилами версий и явными окнами для обновлений.

Ни один выбор не лучше сам по себе. В примере один репозиторий работал хорошо пока одна команда выпускала один продукт. Как только другая команда потребовала иного ритма, версионирование и владение стали важнее скорости рефакторов. Обычно в этот момент форма репозитория начинает влиять на повседневную работу.

Ошибки, которые приводят к неверному разделению

Многие команды дробят репозиторий, потому что он кажется грязным. Обычно это неправильная причина. Грязные папки, медленные тесты и непонятные скрипты сборки — локальные проблемы. Их можно починить без изменения модели релизов.

Противоположная ошибка тоже распространена. Команды держатся одного репозитория слишком долго. Бэкенд выпускает ежедневно, мобильное приложение — раз в две недели, внутренняя админка меняется раз в месяц. Если каждое изменение всё ещё идёт через один и тот же поток релиза, люди начинают ждать работы, которая к ним не относится.

Общие библиотеки создают ещё одну распространённую неудачу. Команда меняет API библиотеки и ожидает, что все сервисы обновятся одновременно. Это может работать некоторое время в одном репозитории. Ломается быстро, когда команды требуют разных сроков обновления. Если вы разделяете — планируйте окно поддержки старых версий. Если не планируете — одно обновление библиотеки может превратиться в неожиданную миграцию.

Копирование кода библиотек — плохой путь ухода. Команды делают это, чтобы избежать споров о владельцах, но счёт приходит позже. Одна копия получает фикс, другая — патч безопасности, третья остаётся старой, потому что про неё забыли. То, что казалось быстрее, становится сложнее отслеживать и дороже поддерживать.

Публикация пакетов имеет скрытую цену. Каждый новый репозиторий добавляет правила версионования, заметки к релизам, контроль доступа, работу по обновлению зависимостей, сломанные пайплайны публикации и совещания «какая у нас версия?». Эти накладные расходы реальны. При решениях о репозиториях команды часто считают границы кода и забывают о повседневной поддержке вокруг них.

Хорошее правило простое: разделяйте, когда расписания релизов, владение и поддержка версий требуют реального разделения. Не дробите только потому, что репозиторий выглядит неряшливо. Сначала почистите код, затем посмотрите, осталась ли боль.

Часто задаваемые вопросы

Как понять, что один репозиторий всё ещё правильный выбор?

Проверьте последние изменения продукта. Если большинство изменений затрагивало несколько приложений или общих пакетов, и одна команда всё ещё могла выпустить их вместе без долгих задержек в ревью, то один репозиторий по‑прежнему подходит.

Если очереди ревью растут, CI по‑лишнему запускает работу, или одна команда часто ждёт релиза другой — текущая структура репозиториев уже отнимает у вас время.

Когда monorepo начинает замедлять команды?

Обычно это начинается, когда изменения в общих частях перестают быть локальными. Небольшая правка библиотеки привлекает лишних ревьюверов, запускает большие CI‑прогоны и заставляет сторонние команды прогонять тесты.

Вы заметите это в ежедневной работе раньше, чем на архитектурной диаграмме: слияния замедляются, релизы собираются в пучки, а люди избегают мелких правок из‑за дорогого пайплайна.

Всегда ли много репозиториев лучше по мере роста команды?

Нет. Много репозиториев полезны, когда команды выпускают в разном ритме, а общий код меняется редко.

Но они добавляют постоянную работу: кому‑то нужно выпускать версии, публиковать пакеты, обновлять зависимости и следить за порядком релизов. Если команды по‑прежнему часто меняют одни и те же рабочие процессы, много репозиториев может добавить больше бюрократии, чем пользы.

Что на самом деле вызывает релизную связь?

Релизная связь возникает, когда одно изменение требует, чтобы несколько команд двигались синхронно. Общие типы, клиенты и широко используемые библиотеки быстро создают такое давление.

Количество репозиториев не убирает зависимость. Один репозиторий проявляет боль через CI и ревью, много репозиториев — через версионирование, апдейты и порядок релизов.

Могу ли я сократить расходы CI без разделения репозитория?

Настройте пайплайн прежде чем дробить код. Используйте правила по путям, кэшируйте зависимости, запускайте только затронутые тесты и перестаньте пересобирать приложения, которые не затронуты изменением.

Проверьте человеческие задержки, а не только минуты раннера. Если инженеры ждут из‑за флейки или широких прогонов, сначала это почините. Чистый пайплайн часто даёт больше времени, чем реорганизация репозиториев.

Кто должен владеть общими библиотеками?

Назначьте каждой общей библиотеке небольшую группу владельцев. Эти люди должны утверждать изменения публичного API, решать время релизов и писать заметки по миграции.

Не оставляйте общие модули открытыми для правки всеми без ответственного — команды будут быстро править, но никто не будет убирать технический долг или решать проблемы, которые появятся у зависимых команд.

Стоит ли разделять репозиторий только потому, что он кажется грязным?

Чаще всего — нет. Плохая структура папок, слабые тесты и запутанные скрипты сборки — это проблемы кодовой базы, а не архитектуры репозиториев.

Сначала почистите код. Если после этого команды всё ещё блокируют друг друга, тогда разделение репозитория решит реальную проблему, а не спрячется за маской.

Какая дополнительная работа связана с версионированием и публикацией пакетов?

Каждому общему пакету нужны правила версий, заметки к релизам, публикация, обновления зависимостей и планы отката. Одно это не кажется большим, но если делать это каждую неделю, накладные расходы складываются.

Эта работа чаще проявляется в инженерных часах, а не в настройках — люди тратят время на проверку совместимости, погоню за старыми версиями и исправление незавершённых кросс‑репозиторных изменений.

Как безопасно протестировать разделение репозитория?

Выделите одну общую библиотеку с чёткими границами и переместите только её. Наблюдайте за скоростью ревью, количеством сломанных сборок, вопросами поддержки и тем, как часто команды ждут друг друга в течение нескольких недель.

Маленький пилот даёт реальные данные. Если разделение уменьшает блокировки — идите дальше. Если добавляет версионирования без пользы — остановитесь до того, как переделаются все репозитории.

Когда нам стоит оставаться с monorepo?

Оставайтесь с monorepo, когда одни и те же люди владеют большей частью продукта, команды выпускают в одном ритме, и общий код меняется часто. В такой ситуации скоординированные правки в одном pull request экономят много времени.

Это также работает, если у каждой области есть явный владелец, а CI умеет выбирать, что запускать. Без этих двух правил удобство быстро испарится.