javascript

Монорепозиторий — стрем или норм?

  • понедельник, 4 мая 2026 г. в 00:00:04
https://habr.com/ru/articles/1030864/
монорепа
монорепа

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

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

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

И в какой-то момент:

  • поддержка начинает стоить дороже разработки

  • маленький фикс превращается в квест

  • новые разработчики боятся трогать код, потому что «тут всё связано со всем»

Это не признак плохой команды. Чаще всего - это следствие быстрого роста и агрессивного time to market.

И с этим в любом случае нужно что-то делать.

Варианты обычно такие:

Скрытый текст

Я люблю вызовы, так что вариант был один - вернуть системе управляемость

Давайте вспомним Марио, в начале все просто - он бегает, прыгает, собирает монетки и топчет врагов. Механики изолированы, логика линейна, всё понятно.

Но вот приходит бизнес и говорит: 

  • Нужно снизить порог входа — добавим вводные уровни

  • Нужны полеты

  • Пусть будет разный транспорт

  • И давайте ещё PvP

И вот скорость персонажа зависит от бонусов, костюмов и режима игры, движение зависит от среды — суша, вода, воздух, враги зависят от атмосферы уровня, добавление нового персонажа задевает половину механик.

И формально это всё ещё одна игра. Монолит жив. Но теперь маленький фикс должен сопровождаться тестированием всей игры, добавление нового персонажа похоже на хоррор, так как он  заденет старые механики, да и новые разработчики боятся трогать код, потому что тут “все связано со всем” 

Проблема не в монолите. Монолит сам по себе - не зло, он прост, быстр и отлично работает на старте. Проблема в неуправляемой связности.

И тут вам нужно найти какой-то мост, между хаотичным монолитом и управляемой системой. Вместо одной огромной коробки, нужно нарисовать “карту мира” разбитую на логические зоны.

Для решения задачи я взяла три подхода:

  • приватные npm пакеты

  • GitLab Submodules

  • Монорепозиторий

npm пакеты

Gitlab Submodules

✅ строгое версионирование
✅ простое поделючение
✅ независимые релизы

НО

❌ нужно публиковать версии
❌ дополнительная инфраструктура registry
❌ на каждое исправление - новый релиз

как приятный бонус - можно получить version hell

✅ хранится как отдельный репозиторий
✅ не нужен npm registry
✅ можно зафиксировать конкретный коммит

НО

❌ часто ломает DX разработчиков
❌ сложное обновление
❌ плохое масштабирование

Оба подхода работают, но начинают ломаться при росте. Npm-пакеты и git submodules плохо масштабируются не технически, а организационно. Они предполагают дисциплину, ручное управление и синхронизацию людей — а именно это ломается первым при росте.

А теперь посмотрим на такую структуру проекта: 

├── apps/
├──├── game/
├──├── admin/
├── packages/
├──├── core/      цикл игры, события, тайминги
├──├── physics/   прыжки, гравитация, столкновения
├──├──characters/ Марио, враги, NPC
├──├── levels/    генерация и правила уровней
├──├── ui/        интерфейс 

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

Монорепозиторий дает нам привилегии: 

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

  2. Общие зависимости ( все общие модули хранятся в packages и переиспользуются, нет необходимости публиковать пакеты и управлять версиями, а обновления доступны сразу всем потребителям) 

  3. Управление сборкой: Turborepo понимает, какие части кода изменились и какие пакеты зависят от них, и пересобирает только их, экономя время.

  4. CI/CD общий, можно настроить единые тесты, линтеры и сборку для всех проектов

  5. Ну и улучшенный developer experience: команды видят весь код сразу, а изменения можно вносить сразу в несколько проектов

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

Думаю, что никакой необходимости рассказывать о том, как переехать на монорепозиторий нет, все можно почитать в подродной документации, но вот, что точно нужно понимать, перед тем как начать с ней работать:

Монорепозиторий = зафиксированная архитектура

В монорепозитории конфигурация - это не техническая деталь, а способ зафиксировать архитектуру проекта. Если архитектурное правило не зафиксировано в конфигурации, значит его не существует. Именно это отличает рабочий монорепозиторий от большого монолита в одном репо.

В целом монорепозиторий - это не про папки, это про ответственность. Есть слой приложений. Есть слой библиотек. Есть направление зависимостей.

├── apps/
├──├── game/ сборка всего вместе
├── packages/
├──├── core/ ядро игры
├──├── physics/  чистая физика
├──├── ui/ интерфейс
пример package.json
пример package.json

Пример правила:

  • приложения могут зависеть от пакетов

  • пакеты не знают о приложениях

  • core не знает про ui

physics — нижний слой: от него могут зависеть другие, но он сам не зависит от доменных модулей.

Это не договоренность в голове. Это правило, зафиксированное в конфигурации.

Dependency graph как документация.

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

  • видно, кто от кого зависит

  • понятно, что собирается раньше

  • видно критический путь

  • видно точки синхронизации

  • видно, где возможен параллелизм

Даже не открывая код, можно понять архитектуру проекта.

├── apps/
├──├── game/ 
├── packages/
├──├── core/ 
├──├── physics/ 
├──├── ui/ интерфейс

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
        "@game/physics#build": {
                "outputs": ["dist/**"]                   //physics
         },                                              //   ↓
        "@game/core#build": { 
                 "dependsOn": ["@game/physics#build"],   // core
                 "outputs": ["dist/**"]                  //   ↓    
         },  
         "@game/ui#build": { 
                 "dependsOn": ["@game/core#build"],      //  ui
                 "outputs": ["dist/**"]                  //   ↓
        },
         "@game/game#build": {                           //  game
                 "dependsOn": [
                         "@game/core#build",
                         "@game/physics#build",
                         "@game/ui#build"
                    ],
                   "outputs": [".next/**"]
       }
    }
}

Без явного графа зависимостей CI запускает всё, не понимает, что реально изменилось и тратит 90% времени впустую. В монорепозитории один коммит влияет на всех, один пайплайн отвечает за всё, один флейк блокирует всю команду.

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

 Окей, а что конфигурация точно не решает?

 Она не запрещает импорты между слоями — это задача линтера.
Она не гарантирует соблюдение слоёв на уровне кода.
Она проверяет зависимости между задачами, но не между модулями.
Она ловит циклы в task graph, но не циклы в import'ах.
Она не знает, что такое apps и packages — порядок сборки возникает только из реальных зависимостей.
 Она не фиксирует бизнес-границы.
Не делает код модульным автоматически.
Не предотвращает архитектурное гниение.
Не заменяет коммуникацию и ревью.
И не спасает от неправильной декомпозиции.

Конфигурация не лечит архитектуру. Она делает её явной и неизбежной.

Но есть еще реальные боли, о которых редко говорят. 

Первый эффект после «вау, как быстро» — это деградация:

  • пайплайны становятся медленными

  • появляются флейки

  • тесты начинают отключать

  • разработчики перестают доверять CI

Причина почти всегда одна: CI не понимает архитектуру репозитория.

Почему это особенно опасно в монорепозитории? Как было сказано выше - один коммит влияет на все, один пайплайн отвечает за все, один сбой блокирует все.

Какие причины? 

CI запускает сборку всех apps, тесты всех packages, линт всего репозитория. И например, вы добавили файл packages/physics/jump.ts, а это запускает билд всех packages + тесты. То есть 90% работы - лишняя, а время CI будет расти линейно с размером репозитория. 

Также берем во внимание, что неявный граф зависимостей - это очень, ОЧЕНЬ ПЛОХО. Ваш CI не будет без него знать, что реально затрагивается при изменении чего-либо. Ну и неправильный кэш (при отсутствии outputs в конфиге), глобальные тесты или один пайплайн на все (линт, тесты, билд, деплой) - все это неизбежно приведет вас к проблемам с CI, потому что он не понимает архитектуру репозитория. 

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

Боль миграции

Монорепозиторий часто воспринимают как то так:

мы сейчас просто разложим монолит по папкам и будет хорошо.

Но на практике это не так - на практике в монолите уже существуют архитектурные зависимости, просто они неявные, а миграция делает существующие проблемы видимыми.

Допустим, у вас в монолите core импортирует ui, но это нигде не зафиксировано, кроме головы разработчика. И пока все в одном проекте - это работает. Но вот вы мигрируете и получаете - циклы, ошибки сборки, неожиданные зависимости. И вам уже приходится не просто перемещать ваш код, а переписывать его. 

Границы пакетов становятся неочевидными, не понятно - где заканчивается core, где начинается features, и что должно быть в ui.

Типичная ошибка разработчиков - они режут пакеты “по папкам”, а не “по ответственности”, и взамен получают перекрестные зависимости и постоянные рефакторинги структуры. Ну и все это неизбежно ведет к сопротивлению команды - не все понимают ценность разделения, кажется, что становится сложнее работать и появляются обходные пути и лазейки в архитектуре. (игнорирование правил, локальные копии shared слоя, дублирование кода)

Tooling — это инфраструктура. 

​​В монорепе package manager — не личный выбор, TypeScript требует централизованного конфига, eslint должен быть контекстным. Если этого нет: локально все работает, а в CI падает. Как следствие разработчики отключают правила и инструменты превращаются в шум

Итого: когда монорепозиторий - норм?

если у вас :

  • несколько приложений

  • общий домен

  • команда готова договариваться

  • CI — часть архитектуры

Монорепозиторий — это не про скорость. И не про хайп. Это инструмент управления сложностью. И если вы готовы зафиксировать границы, описать зависимости и инвестировать в CI - он станет точкой роста. Если нет, то он просто сделает хаос очевидным.

И это, кстати, тоже полезно.