Наверное, у каждого разработчика был момент, когда бизнеса в жизни становится слишком много. Слишком много хотелок. Слишком короткие сроки. Слишком мало времени подумать.
И в этот момент код перестаёт быть инженерной задачей. Он превращается в бесконечное тушение пожаров.
Требования меняются быстрее, чем ты успеваешь их осмыслить. Приоритеты «на вчера». Технический долг растет не потому, что вы плохие разработчики, а потому что у вас просто нет времени быть аккуратными.
И в какой-то момент:
-
поддержка начинает стоить дороже разработки
-
маленький фикс превращается в квест
-
новые разработчики боятся трогать код, потому что «тут всё связано со всем»
Это не признак плохой команды. Чаще всего — это следствие быстрого роста и агрессивного time to market.
И с этим в любом случае нужно что-то делать.
Варианты обычно такие:

Скрытый текст
Я люблю вызовы, так что вариант был один — вернуть системе управляемость
Давайте вспомним Марио, в начале все просто — он бегает, прыгает, собирает монетки и топчет врагов. Механики изолированы, логика линейна, всё понятно.
Но вот приходит бизнес и говорит:
-
Нужно снизить порог входа — добавим вводные уровни
-
Нужны полеты
-
Пусть будет разный транспорт
-
И давайте ещё PvP
И вот скорость персонажа зависит от бонусов, костюмов и режима игры, движение зависит от среды — суша, вода, воздух, враги зависят от атмосферы уровня, добавление нового персонажа задевает половину механик.
И формально это всё ещё одна игра. Монолит жив. Но теперь маленький фикс должен сопровождаться тестированием всей игры, добавление нового персонажа похоже на хоррор, так как он заденет старые механики, да и новые разработчики боятся трогать код, потому что тут “все связано со всем”
Проблема не в монолите. Монолит сам по себе — не зло, он прост, быстр и отлично работает на старте. Проблема в неуправляемой связности.
И тут вам нужно найти какой-то мост, между хаотичным монолитом и управляемой системой. Вместо одной огромной коробки, нужно нарисовать “карту мира” разбитую на логические зоны.
Для решения задачи я взяла три подхода:
-
приватные npm пакеты
-
GitLab Submodules
-
Монорепозиторий
|
npm пакеты |
Gitlab Submodules |
|
✅ строгое версионирование НО ❌ нужно публиковать версии как приятный бонус — можно получить version hell |
✅ хранится как отдельный репозиторий НО ❌ часто ломает DX разработчиков |
Оба подхода работают, но начинают ломаться при росте. Npm-пакеты и git submodules плохо масштабируются не технически, а организационно. Они предполагают дисциплину, ручное управление и синхронизацию людей — а именно это ломается первым при росте.
А теперь посмотрим на такую структуру проекта:
├── apps/├──├── game/├──├── admin/├── packages/├──├── core/ цикл игры, события, тайминги├──├── physics/ прыжки, гравитация, столкновения├──├──characters/ Марио, враги, NPC├──├── levels/ генерация и правила уровней├──├── ui/ интерфейс
Формально — у нас все так же один репозиторий, но код перестает быть кашей, у него появляются границы и зоны ответственности.
Монорепозиторий дает нам привилегии:
-
Единая структура — весь код находится в одном репозитории, есть прозрачная иерархия и онбординг новых сотрудников становится проще — один репозиторий, один способ сборки, одни правила.
-
Общие зависимости ( все общие модули хранятся в packages и переиспользуются, нет необходимости публиковать пакеты и управлять версиями, а обновления доступны сразу всем потребителям)
-
Управление сборкой: Turborepo понимает, какие части кода изменились и какие пакеты зависят от них, и пересобирает только их, экономя время.
-
CI/CD общий, можно настроить единые тесты, линтеры и сборку для всех проектов
-
Ну и улучшенный developer experience: команды видят весь код сразу, а изменения можно вносить сразу в несколько проектов
Тут необходимо остановиться и внести ясность, монорепозиторий — это не панацея, но для нужд быстрого перехода на более стабильную архитектуру, а также с учетом задачи и условий работы команды, монорепозиторий стал наиболее подходящим вариантом решения проблемы.
Думаю, что никакой необходимости рассказывать о том, как переехать на монорепозиторий нет, все можно почитать в подродной документации, но вот, что точно нужно понимать, перед тем как начать с ней работать:
Монорепозиторий = зафиксированная архитектура
В монорепозитории конфигурация — это не техническая деталь, а способ зафиксировать архитектуру проекта. Если архитектурное правило не зафиксировано в конфигурации, значит его не существует. Именно это отличает рабочий монорепозиторий от большого монолита в одном репо.
В целом монорепозиторий — это не про папки, это про ответственность. Есть слой приложений. Есть слой библиотек. Есть направление зависимостей.
├── apps/├──├── game/ сборка всего вместе├── packages/├──├── core/ ядро игры├──├── physics/ чистая физика├──├── ui/ интерфейс
Пример правила:
-
приложения могут зависеть от пакетов
-
пакеты не знают о приложениях
-
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 — он станет точкой роста. Если нет, то он просто сделает хаос очевидным.
И это, кстати, тоже полезно.
ссылка на оригинал статьи https://habr.com/ru/articles/1030864/