Монорепо против polyrepo: как держать web, mobile и backend в синхроне
Монорепо против polyrepo — объяснение для команд, выпускающих web, mobile и backend. Сравните управление зависимостями, координацию релизов и тактики CI, чтобы сохранять скорость.

Настоящая проблема: выпуск изменений в трёх кодовых базах
Команды спорят о монорепо против polyrepo не из‑за философии Git. Они спорят потому, что небольшое изменение продукта превращается в три отдельных бага в web, mobile и backend, и что‑то ломается по пути.
Первым ломается редко UI. Чаще страдает невидимый клей: контракт API изменили без соответствующего обновления, общая библиотека обновили в одном месте, но забыли в другом, или конвейер сборки внезапно потребовал новый шаг. Когда одна часть выходит раньше других, пользователи видят баги вроде «кнопка есть на web, а мобильное приложение говорит «не поддерживается»» или «приложение висит на загрузке, потому что ответ бэкенда изменился».
Web, mobile и backend работают в разных тактах релизов. Web может выходить по несколько раз в день. Backend тоже может часто релизиться, но требует осторожного роллаута. Mobile самый медленный — проверка в магазине приложений и обновления пользователей добавляют реальную задержку. «Простое» изменение, например переименование поля, может заставить вас планировать по самому медленному каналу, даже если это нужно только для одного экрана.
Вы, скорее всего, платите налог координации репозиториев, если это происходит регулярно:
- Ломание API обнаруживается после мерджа.
- Согласование версий зависит от ручных напоминаний и таблиц.
- Одна фича требует нескольких скоординированных pull request’ов, которые ждут друг друга.
- CI медленный, потому что собирает и тестирует гораздо больше, чем было затронуто изменением.
- Откаты рискованы, потому что непонятно, какой коммит соответствует какому релизу.
Размер команды и зрелость продукта влияют на то, что правильно. На ранних этапах большинству команд выгодно сделать координацию дешёвой и видимость высокой, даже если будет немного беспорядка. По мере роста границы начинают иметь значение, но только если интерфейсы стабильны и ответственность ясна.
Если каждое значимое изменение должно попасть в три места, вы как‑то заплатите этот налог. Стратегия репозиториев в основном о том, как вы хотите его оплачивать.
Основы монорепо и polyrepo без жаргона
Репозиторий — это просто место, где живёт ваш код и история. Когда у вас есть web, mobile и backend, выбор прост: держать всё вместе или разделять.
Монорепо — один репозиторий, содержащий несколько приложений и часто общий код. Web, iOS/Android, сервисы backend и общие библиотеки живут бок о бок.
Polyrepo — противоположный подход: каждое приложение (а иногда и каждый сервис) в своём репозитории. Общий код обычно превращается в отдельный пакет или команды копируют небольшие кусочки при необходимости.
В повседневной работе монорепо обычно ощущается так: шарить код легко, кросс‑апп изменения могут быть в одном pull request, правила едины. Компромисс — социальный: владение может размыться, если не задать границы, а проверки на уровне репозитория могут казаться строгими.
Polyrepo ощущается так: каждая команда двигается независимо, репозитории остаются сфокусированными, и контроль доступа проще. Компромисс — координация: шаринг кода требует планирования, и кросс‑апп изменения часто превращаются в несколько PR с аккуратным таймингом.
Многие команды приходят к гибриду: приложения в отдельных репозиториях, общие контракты в одном месте; или одно монорепо с жёсткими границами, чтобы каждая команда в основном работала в своей зоне.
Если вы используете платформу, которая генерирует backend, web и mobile из одного источника правды, вы сокращаете дрейф, потому что контракты и логика живут вместе. AppMaster, например, генерирует production-ready backend, web и нативные мобильные приложения из единой модели. Это не отменяет реалии релизов (mobile всё равно медленнее), но может устранить большую часть рутины «обновили ли мы все три места?».
Управление зависимостями: как обезопасить общий код
Именно на общем коде команды теряют время, независимо от схемы репозиториев. Небольшое изменение в общей библиотеке или контракте API может по‑разному ломать web‑сборки, мобильные релизы и деплои backend.
Когда вы делитесь библиотеками (UI‑компоненты, правила валидации, хелперы аутентификации), вы выбираете между одной версией для всех и несколькими версиями со временем.
- Одна версия проще и исключает сюрпризы «работает в моей ветке».
- Несколько версий дают командам свободу темпа, но создают шум и усложняют распространение исправлений безопасности.
Клиенты API и схемы требуют особого внимания. Ручные обновления медленные и склонны к ошибкам. Лучший паттерн — считать схему API источником правды и генерировать клиентов из неё, затем либо чекать их в репозиторий, либо генерировать в CI. Цель — быстро падать: если backend добавил обязательное поле, мобильный клиент должен падать на сборке, а не через три дня в QA.
Ломкие изменения распространяются, когда поведение меняют без безопасного пути миграции. Предпочитайте добавочные изменения (новые поля, новые эндпоинты), а потом депрекейтьте. Если нужно ломать, используйте версионированные эндпоинты или короткое окно совместимости.
Конкретный пример: backend переименовал status в state. Если web обновился сегодня, а мобильное приложение не может выйти неделю, backend должен принимать оба поля в течение этой недели или выпустить адаптер, который маппит старое поле на новое.
Несколько правил, которые делают обновления зависимостей скучными (в хорошем смысле):
- Обновляйтесь по расписанию. Маленькие еженедельные обновления лучше больших квартальных.
- Требуйте явного одобрения для ломающих изменений и короткой заметки о миграции.
- Автоматизируйте проверки: обновления зависимостей, регенерация клиентов и базовые контрактные тесты.
- Определяйте «готово» как «web, mobile и backend собираются и проходят тесты», а не «мой репозиторий в порядке».
Генерируемый код уменьшает дрейф, но не отменяет дисциплину. Нужны единый контракт, понятные deprecation‑правила и предсказуемые обновления.
Координация релизов: синхронизируем web, mobile и backend
Координация релизов — это то место, где стратегия репозиториев перестаёт быть теоретической. Если backend меняет имя поля API, web чаще всего может обновиться и выпустить в тот же день. Mobile другое: проверка в магазине и время обновления пользователей могут превратить маленькое рассинхронирование в неделю тикетов поддержки.
Практическая цель проста: действие пользователя должно работать независимо от того, какая часть обновилась первой. Это значит планировать под смешанные версии, а не надеяться на идеально синхронный релиз.
Паттерны версионирования, которые реально используют команды
Большинство команд останавливаются на одном из подходов:
-
Единый релизный поезд: web, mobile и backend релизятся как единый версионированный блок.
-
Версии по сервисам с правилами совместимости: у каждого сервиса/приложения своя версия, а backend поддерживает определённый диапазон клиентских версий.
Единый релизный поезд выглядит аккуратно, но часто ломается из‑за задержек мобильных релизов. Версии по сервисам на бумаге выглядят путаче, но соответствуют реальности. Если вы выбираете такой подход, зафиксируйте одно правило и соблюдайте его: какие версии backend должны поддерживать какие мобильные версии и сколько времени.
Задержки мобильных релизов также влияют на хотфиксы. Backend‑хотфиксы можно выпустить быстро; мобильные хотфиксы могут не дойти до пользователей несколько дней. Предпочитайте сервер‑сайд фиксы, которые поддерживают старые мобильные сборки. Когда нужно менять клиент, используйте feature flags и не убирайте старые поля, пока не будет ясно, что большинство пользователей обновились.
Пример: вы добавляете «инструкции по доставке» в flow заказа. Backend добавляет новое опциональное поле, web показывает его сразу, mobile — в следующем спринте. Если backend принимает старые запросы и поле остаётся опциональным, всё работает, пока mobile догоняет.
Кто владеет календарём релизов
Координация проваливается, когда «владельцем является кто угодно», поэтому никто. Владелец может быть tech lead, release manager или product manager с сильной инженерной поддержкой. Его задача — предотвращать сюрпризы, делая ожидания по релизам видимыми и предсказуемыми.
Им не нужен сложный процесс. Нужны повторяемые привычки: простой календарь релизов с cutoff’ами и freeze‑окнами, быстрая кросс‑командная проверка перед изменением API и ясный план на случай задержки мобильного релиза (удержать backend‑изменение или сохранить совместимость).
Если ваш рабочий поток генерирует web, mobile и backend из одной модели, владелец всё равно нужен. Число «обновили ли мы все три места?» снизится, но вы не избежите тайминга мобильных релизов.
Как держать CI под контролем
CI медленно работает по тем же причинам в обоих подходах: вы пересобираете слишком много, повторно качаете зависимости и запускаете все тесты на каждое изменение.
Распространённые виновники: полные билды при мелких изменениях, пропущенные кеши, тест‑сьюты, которые запускают всё, и последовательные джобы, которые можно распараллелить.
Начните с общих улучшений, которые помогают везде:
- Кешируйте скачанные зависимости и артефакты сборки.
- Запускайте lint, unit‑тесты и сборки параллельно, где возможно.
- Разделяйте быстрые проверки (на каждый коммит) и медленные (ветка main, nightly или перед релизом).
Тактики для монорепо
Монорепо становится тяжёлым, когда каждый коммит запускает pipeline «построй мир». Решение — билдить и тестировать только то, что затронуто изменением.
Используйте фильтры по путям и подход «только затронутые части»: если вы изменили код мобильного UI, не пересобирайте образы backend. Если тронули общую библиотеку, билдьте и тестируйте только приложения, которые от неё зависят. Многие команды формализуют это простой граф‑зависимостей, чтобы CI мог принимать решение вместо догадок.
Тактики для polyrepo, которые предотвращают дрейф
Polyrepo может быть быстрым, потому что каждый репозиторий меньше, но часто теряет время из‑за дублирования и несовместимых инструментов.
Держите один набор шаблонов CI (одни и те же шаги, одни и те же кеши, одни и те же соглашения), чтобы не изобретать пайплайн в каждом репозитории. Зафиксируйте toolchain (версии рантаймов, билд‑инструменты, линтеры), чтобы избежать сюрпризов «работает в одном репозитории». Если скачивание зависимостей — узкое место, заведите общий кэш или внутреннее зеркало, чтобы каждый репозиторий не тянул всё из интернета.
Конкретный пример: фича добавляет новое поле «status». Backend меняется, web показывает его, mobile показывает позже. В монорепо CI должен запустить тесты backend и только те web и mobile части, которые зависят от клиента API. В polyrepo каждый репозиторий запускает свои быстрые проверки, а отдельный интеграционный pipeline валидирует, что три релиза совместимы.
Если вы экспортируете исходники и запускаете собственный CI, правило одно: билдьте только изменившееся, агрессивно используйте кеши и оставляйте медленные проверки для того, где они приносят ценность.
Пошагово: выбираем стратегию репозитория, подходящую команде
Решение проще, если вы начинаете с того, как вы работаете каждый день, а не с идеологии.
1) Запишите, что должно меняться вместе
Выберите 5–10 недавних фич и отметьте, что приходилось менять синхронно. Укажите, затрагивались ли UI‑экраны, API‑эндпоинты, таблицы данных, правила аутентификации или общая валидация. Если большинство фич требует координации по всем трём зонам, разделённая схема будет болезненной, если только у вас нет очень дисциплинированного процесса релизов.
2) Проследите общий код и общие решения
Общий код — это не только библиотеки. Это ещё контракты (схемы API), UI‑паттерны и бизнес‑правила. Запишите, где это живёт сейчас, кто это меняет и как согласуются изменения. Если общие куски копируются между репозиториями, это знак, что нужно ужесточить контроль через монорепо или строгие правила версионирования.
3) Определите границы и владельцев
Решите, какие единицы вы принимаете (приложения, сервисы, библиотеки), затем назначьте владельца для каждой. Границы важнее, чем расположение кода. Без владельцев монорепо превращается в шум. Без владельцев polyrepo теряет связь.
Если нужно простое чек‑лист‑правило: один репозиторий или папка на деплой‑юнит, одно место для общих контрактов, одно место для действительно общих UI‑компонентов, ясное правило, где живёт бизнес‑логика, и задокументированный владелец для каждого.
4) Выберите модель релизов, которой вы сможете следовать
Если мобильные релизы отстают от backend, нужна план совместимости (версионированные API, обратная совместимость полей или определённое окно поддержки). Если всё должно выходить вместе, релизный поезд работает, но увеличивает координацию.
Держите правила ветвления скучными: короткоживущие ветки, маленькие мерджи и понятный путь для хотфиксов.
5) Спроектируйте CI вокруг частых изменений
Не проектируйте CI под худший сценарий в первый день. Проектируйте его под то, что люди делают каждый день.
Если большинство коммитов затрагивает только web UI, по умолчанию запускайте web lint и unit‑тесты, а полные end‑to‑end тесты — по расписанию или перед релизом. Если инциденты чаще от дрейфа API, вложите усилия сначала в контрактные тесты и регенерацию клиентов.
Пример: одна фича, затрагивающая web, mobile и backend
Представьте небольшую команду, строящую три продукта: портал для клиентов (web), полевой апп (mobile) и API (backend). Поступила просьба: добавить новое поле «Service status» в jobs и показывать его везде.
Изменение кажется маленьким, но это тест на координацию. Backend добавляет поле, обновляет валидацию и ответы. Web отображает поле и обновляет фильтры. Mobile должен показать его оффлайн, синхронизировать и обработать крайние случаи.
Проблема реальна: изменение API ломающее. Имя поля меняется с status на service_status, и старые клиенты крашатся, если не обрабатывают новое.
Что меняет монорепо
Здесь монорепо часто кажется спокойнее. Обновления backend, web и mobile могут попасть в один PR (или в один набор координированных коммитов). CI запускает тесты для затронутых частей, и вы можете пометить релиз, который содержит все три обновления.
Главный риск — социальный, а не технический: в одном репозитории проще быстро смерджить ломающее изменение, поэтому правила ревью должны быть строгими.
Что меняет polyrepo
С отдельными репозиториями каждое приложение живёт в своём графике. Backend может выйти первым, а web и mobile поспешно догоняют. Если mobile‑релизы зависят от проверки магазина, исправление может занять дни, даже если код маленький.
Команды решают это структурно: версионированные эндпоинты, обратно‑совместимые ответы, длинные окна депрекейта и чёткие шаги rollout’а. Это работает, но требует постоянной работы.
Если вы решаете на основе данных, посмотрите последние месяцы:
- Частые инциденты из‑за несовпадения версий → краща координация.
- Частые и срочные релизы (особенно mobile) → избегайте ломких изменений или централизуйте их.
- Команды независимы и редко трогают одни и те же фичи → overhead polyrepo может окупаться.
Распространённые ошибки и ловушки
Большинство команд не проваливаются из‑за «неправильного» выбора структуры репозиториев. Они проваливаются, потому что повседневные привычки постепенно добавляют трение, пока каждое изменение не становится рискованным.
Общий код превращается в свалку
Общая библиотека привлекает: хелперы, типы, UI‑кусочки, «временные» обходы. Скоро это место, куда прячут старый код, и никто не знает, что безопасно менять.
Держите общий код маленьким и строгим. «Общий» должен означать «используется многими командами», тщательно ревьюиться и меняться осознанно.
Сильная связанность через скрытые предположения
Даже в отдельных репозиториях системы могут быть сильно связаны. Связность просто переходит в предположения: форматы дат, значения enum’ов, правила прав доступа и «это поле всегда присутствует».
Пример: mobile считает status = 2 как «Approved», web — как «Confirmed», backend меняет порядок enum’ов, и всё ломается непредсказуемо.
Предотвращайте это документируя контракты (что означают поля, какие значения разрешены) и относитесь к ним как к продуктовым правилам, а не к незначительным деталям.
Неопределённое владение
Когда каждый может менять всё, ревью становятся поверхностными и ошибки проходят. Когда никто не владеет областью, баги лежат неделями.
Определите владельцев для web, mobile, backend и общих модулей. Владение не блокирует вкладчиков, оно гарантирует, что изменения получают правильные глаза.
CI растёт без ухода
CI часто начинается маленьким, а потом каждое происшествие добавляет новый job «чтобы не рисковать». Через месяцы он медленный и дорогой, и люди начинают его избегать.
Простое правило: у каждого CI‑job’а должна быть ясная цель и владелец, и его нужно удалить, если он перестал ловить реальные проблемы.
Признаки, что нужен рефактор: дублирующиеся тесты в разных job’ах, джобы, которые краснеют днями, «быстрое изменение», проверка которого занимает дольше, чем сборка, и пайплайны, которые запускают мобильные билды по backend‑изменениям.
Координация релизов на уровне «племенных знаний»
Если релизы зависят от одного человека, помнящего порядок и секретные нюансы, вы будете релизиться медленнее и чаще ломать вещи.
Запишите шаги релиза, сделайте их повторяемыми и автоматизируйте скучные проверки. Даже если инструменты генерируют согласованные backend и клиенты, вам всё равно нужны ясные правила релиза.
Быстрая проверка перед решением о монорепо или polyrepo
Перед реорганизацией репозиториев проверьте, как команда сейчас релизит. Цель не в идеальной структуре, а в меньшем числе сюрпризов, когда изменение затрагивает web, mobile и backend.
Задайте пять вопросов:
- Независимый релиз: можно ли выпустить исправление backend без принуждения к обновлению мобильного приложения в тот же день?
- Правила изменения API: есть ли задокументированная политика для депрекейтов и сколько времени старое поведение остаётся поддерживаемым?
- Дисциплина общего кода: ревьюятся ли и версионируются ли общие библиотеки (UI, клиенты API, бизнес‑правила) последовательно?
- CI, который запускает важное: может ли CI понять, что изменилось, и запустить билды/тесты только для затронутых частей?
- Единый обзор релизов: есть ли одно место, где видно, что выходит в web, mobile и backend, с владельцами и датами?
Простой пример: добавляется поле «address» в checkout. Если backend выходит первым, старое мобильное приложение должно продолжать работать. Это обычно значит, что API принимает и старую, и новую полезную нагрузку какое‑то время, а обновления клиентов опциональны, а не обязательны.
Следующие шаги: уменьшить работу по координации и выпускать уверенно
Цель — не «правильная» структура репозитория. Цель — меньше передач, меньше сюрпризов и меньше моментов «какая версия сейчас жива?».
Напишите короткий документ с решением: почему вы выбрали текущий подход, что вы ожидаете улучшить и какие компромиссы принимаете. Пересматривайте его каждые 6–12 месяцев или раньше, если размер команды или частота релизов изменится.
Прежде чем перемещать файлы, выберите наименьшее изменение, которое убирает реальную боль:
- Введите и соблюдайте правила версионирования общих пакетов.
- Определите контракты API и заставьте их проверяться контрактными тестами в CI.
- Согласуйте единый чек‑лист релиза для web, mobile и backend.
- Используйте preview‑среды для изменений, затрагивающих несколько частей.
- Установите лимиты времени для CI (например: проверки PR < 15 минут).
Если узким местом является связность между кодовыми базами, уменьшение числа мест, которые нужно править, может быть важнее, чем расположение репозиториев. Некоторые команды решают это, перемещая больше логики и моделирования данных в единый источник правды.
Если хотите исследовать такой подход, AppMaster (appmaster.io) создан для генерации backend‑сервисов, web‑приложений и нативных мобильных приложений с общими моделями данных и бизнес‑логикой. Низко‑рисковый способ оценить — построить один небольшой внутренний инструмент сначала, а затем решить по реальной экономии координации.
Уверенный путь намеренно скучен: задокументируйте решение, уменьшайте связанность, автоматизируйте проверки и меняйте структуру репозитория только тогда, когда метрики покажут, что это поможет.
Вопросы и ответы
Начните с анализа того, как часто одна фича требует изменений в web, mobile и backend одновременно. Если большая часть работы перекрёстная и координация — основная боль, монорепо или сильный подход «единый контракт» обычно уменьшают число ошибок. Если команды редко пересекаются и им нужен независимый доступ и выпуск, polyrepo хорошо работает при строгих правилах совместимости.
Чаще всего виноваты дрейф API, рассинхрон версий общих библиотек и различия в графиках релизов (особенно задержки с публикацией мобильных приложений в магазинах). Решение — планировать смешанные версии в реальности, а не надеяться на идеально синхронные релизы, и делать ломкие изменения редкими и продуманными.
Считайте схему API источником правды и генерируйте клиентские SDK из неё, чтобы рассинхроны проявлялись на этапе сборки, а не в QA или проде. Сначала предпочитайте добавочные изменения, потом депрекейтьте старое, и держите короткое окно совместимости при необходимости переименований или удаления полей.
Стремитесь к небольшим регулярным обновлениям по расписанию (лучше еженедельные, чем квартальные) и требуйте явного одобрения для ломающих изменений. Сопроводите каждое изменение кратким описанием миграции и определяйте «готово» как «web, mobile и backend собираются и проходят тесты», а не только «мой репозиторий в порядке».
По умолчанию чаще всего выбирают модель с версиями по сервисам и чётким правилом совместимости: какие версии backend поддерживают какие клиентские версии и как долго. Общий релизный поезд может работать на ранних этапах, но задержки мобильных релизов часто делают его неудобным, если продукт не может ждать времени магазина приложений.
Держите backend обратно-совместимым, чтобы старые мобильные сборки продолжали работать, пока пользователи обновляются. Используйте добавочные поля, не удаляйте старое поведение слишком рано и применяйте feature flags для поэтапного включения изменений в клиенте.
Назначьте это явно — обычно tech lead, release manager или product owner при сильной инженерной поддержке. Цель — простой, повторяемый календарь релизов и чёткое правило для задержек (удержать изменение или сохранить совместимость), а не тяжёлая бюрократия.
По умолчанию — билдить и тестировать только то, что изменилось, и кешировать всё, что можно. Разделяйте быстрые проверки (на каждый коммит) и медленные (ветка main, nightly или перед релизом), чтобы разработчики получали быстрый фидбек без постоянной платы в виде полного тестового прогона.
Применяйте фильтры по путям и подход «только затронутые части», чтобы не пересобирать весь проект ради маленького изменения. Если изменился общий модуль, запускайте проверки только для приложений, которые от него зависят, и держите правила владения и ревью чёткими, чтобы один репозиторий не превратился в свалку для всех.
Стандартизируйте инструменты и шаблоны CI по всем репозиториям, чтобы каждый не придумывал шаги и кеши заново. Добавьте интеграционную проверку, которая валидирует ключевые контракты между релизами, и зафиксируйте версии toolchain, чтобы избежать «работает в одном репозитории, но не в другом».


