Skip to main content

Monorepo vs Polyrepo - переваги та недоліки?

Monorepo vs polyrepo - monorepo тримає всі проекти в одному Git-репозиторії; polyrepo дає кожному проекту власне репо.

Теорія

TL;DR

  • Monorepo = одна спільна кухня (спільні інгредієнти і інструменти); polyrepo = окремі кухні для кожного шефа, де кожна спеція дублюється
  • Головна різниця: monorepo дозволяє зафіксувати зміну в shared-ui і оновити всі залежні застосунки одним комітом; polyrepo потребує publish-циклу і ручного бампу версій у кожному споживачі
  • Більше 5 взаємозалежних пакетів - вибирай monorepo; незалежні команди з окремими сервісами - вибирай polyrepo
  • Основні інструменти: Nx, Turborepo, Bazel для monorepo; Lerna і git submodules для polyrepo
  • Реальні цифри: Google тримає один Bazel-монорепо на 86TB; Netflix розбиває все на 700+ окремих репо через Spinnaker

Швидкий приклад

Структура monorepo (Nx workspace):

my-company/ apps/ web/ # React-застосунок - імпортує shared-ui напряму api/ # Node.js бекенд libs/ shared-ui/ # UI-компоненти, без кроку публікації nx.json # Будує тільки змінені проекти

Polyrepo:

my-company-web/ # Git repo #1 - бере shared-ui через npm my-company-api/ # Git repo #2 my-company-ui/ # Git repo #3 - публікується в npm registry

В monorepo один git commit оновлює web і shared-ui разом. В polyrepo та ж зміна потребує трьох комітів, npm publish і бампу версій.

Ключова різниця

Справжня різниця між monorepo і polyrepo проявляється при зміні спільної бібліотеки. В monorepo ти оновлюєш shared-ui, і граф залежностей (Nx будує його з tsconfig.json) автоматично позначає web для перебудови. Один PR, одне ревʼю, нульовий version mismatch. В polyrepo та ж зміна потребує публікації нової npm-версії, оновлення version pin у кожному споживачі, очікування CI в кожному репо і координації мерджів між командами. Дрейф версій (version drift) - не теоретична проблема. Це стандартний результат для будь-якої нетривіальної спільної бібліотеки в polyrepo.

Коли що використовувати

Вибирай monorepo якщо:

  • Кілька застосунків поділяють UI-компоненти, утиліти або типи
  • Потрібні атомарні зміни по всьому стеку (оновити API-контракт і фронтенд одним PR)
  • Команда невелика або середня (до 50 розробників) і власнить весь продукт
  • Ти будуєш design system, який використовують 3+ застосунки

Вибирай polyrepo якщо:

  • Команди власнять повністю незалежні сервіси без спільного коду
  • Потрібен суворий контроль доступу на рівні репозиторію (compliance, security isolation)
  • Сервіси деплояться на абсолютно різних каденсах без coupling
  • Ти запускаєш мікросервіси, де кожен сервіс є black box

Порівняльна таблиця

АспектMonorepoPolyrepo
Шерінг кодуПрямі імпорти, без публікаціїnpm-публікація + version pinning
КомітиАтомарні по всіх проектахОкремі PR, синхронізація через теги
Швидкість збіркиКешована/інкрементальна (Turborepo кешує 90%+)Повна перебудова кожного репо
Розмір репоРосте до 100GB+Невеликий (1-5GB кожне)
Контроль доступуШлях-базований через CODEOWNERSДозволи на рівні репо
CI/CDСкладна оркестрація (Nx affected)Прості пайплайни на кожне репо
ІнструментиNx, Turborepo, BazelLerna, git submodules
Хто використовуєGoogle, Meta, MicrosoftNetflix, AWS
Коли використовуватиСпільний код, єдиний продуктАвтономія команд, незалежні сервіси

Як це працює всередині

Git в monorepo обробляє весь репозиторій як єдине дерево. git diff охоплює всі проекти. Nx парсить project.json і tsconfig.paths, будує граф залежностей і запускає nx affected:build тільки для того, що змінилось. Якщо ти торкнувся shared-ui, Nx відстежує, які застосунки його імпортують, і перебудовує лише їх. Google доводить це до крайнощів: Bazel хешує кожен вхідний файл і перевіряє remote cache спочатку. При 95% hit rate збірка 100,000 файлів займає стільки ж часу, що й збірка 1,000.

Polyrepo не має ніякої крос-репозиторної свідомості за замовчуванням. CI запускається незалежно для кожного репо. Коли shared-ui публікує v2.1.0, жодне з залежних репо не отримує автоматичного сповіщення. Команди дізнаються про breaking changes тільки коли їхній CI ламається після оновлення version pin.

Типові помилки

Помилка: запускати повний CI на кожен push в monorepo

Без affected-фільтрації monorepo збирає всі проекти на кожен push. З 10 застосунками це 10-хвилинний CI-пайплайн на однорядкове виправлення.

bash
# Неправильно: перебудовує все npm run build:all # Правильно: перебудовує тільки залежні проекти npx nx affected:build --base=main

Помилка: вважати, що monorepo не підтримує контроль доступу

Це хибне уявлення. GitHub і GitLab підтримують path-based branch protection. Файл CODEOWNERS задає гранулярні вимоги до ревʼю:

# .github/CODEOWNERS /libs/shared-ui/ @ui-team /apps/api/ @backend-team /infra/ @platform-team

Pull request, що торкається libs/shared-ui, автоматично запитує ревʼю від @ui-team. Ніхто поза командою не може замерджити ці зміни без ревʼю. Модель доступу більш гранулярна, ніж дозволи на рівні репо в polyrepo.

Помилка: polyrepo без автоматичного версіонування

Ручні version bumps призводять до зламаних деплоїв. Якщо shared-ui публікує major version, але downstream-команда пропускає бамп, їхня збірка ламається без попередження. Рішення: автоматизуй через conventional commits і npm version в CI.

Помилка: міграція в monorepo через злиття git-історій

Злиття повних історій з 5 репо створює нечитабельний git log з 50,000 комітів непов'язаних проектів. Використовуй git filter-repo для squash або імпортуй кожен проект з чистим початковим комітом.

Помилка: ігнорувати remote caching

Monorepo без remote cache сповільнюється по мірі росту і врешті-решт стає не швидшим за polyrepo. Turborepo Remote Cache або Nx Cloud розподіляють кешовані артефакти між усіма розробниками і CI-машинами. Збірка одного інженера наповнює кеш для наступних десяти.

Де зустрічається на практиці

  • Google використовує Bazel на одному монорепо з 2 мільярдами рядків коду і 120,000 інженерів; зміни в Android і Chrome деплояться одним атомарним комітом
  • Meta запускає Buck на монорепо, що містить React Native і всі вебзастосунки
  • Microsoft використовує Nx для розробки VS Code і TypeScript
  • Netflix керує 700+ окремими репо через Spinnaker для деплой-оркестрації
  • Uber тримає монорепо з 1,000+ пакетами для координації мобільних і вебплатформ

Можливі питання на співбесіді

Q: Як Nx визначає, які проекти зачеплені змінами?
A: Nx парсить project.json і tsconfig.paths і будує граф залежностей. При пуші порівнює поточний коміт з base SHA і відстежує, які проекти імпортують змінені файли.

Q: В чому різниця між Bazel і Turborepo?
A: Bazel є hermetic: збірки відтворювані на будь-якій машині, бо кожен вхід хешується і ізолюється. Turborepo налаштовується набагато швидше і добре підходить для JS-проектів. Bazel має сенс при 100,000+ файлів, Turborepo покриває більшість команд нижче цього порогу.

Q: Як примусово застосовувати межі модулів (module boundaries) в monorepo?
A: Nx надає enforceModuleBoundaries в конфігурації eslint. Це блокує невалідні імпорти між пакетами на основі тегів, що ти призначаєш кожному проекту. Наприклад, web може імпортувати shared-ui, але не api.

Q: Як команди в polyrepo можуть шерити код без переходу на monorepo?
A: Через приватний npm registry: публікуй пакети і бери їх як версійні залежності. Компроміс - publish-update-merge цикл на кожну спільну зміну. Git submodules є альтернативою, але вони повільні і схильні до проблем зі станом.

Q: Як масштабувати monorepo до тисяч розробників?
A: Google вирішує це через Piper (внутрішня VCS, що обробляє 1 мільйон комітів на день), path-based ACL і Bazel remote execution (RBE), який розподіляє кроки збірки по кластеру. Для менших масштабів Nx Cloud або Turborepo remote caching покривають основне вузьке місце.

Приклади

Шерінг утиліти через Turborepo

json
// apps/web/package.json { "dependencies": { "@my-company/shared-utils": "workspace:*" } }
ts
// apps/web/src/api.ts import { formatDate } from '@my-company/shared-utils'; const display = formatDate(new Date()); // "2024-01-15T00:00:00.000Z"
ts
// packages/shared-utils/src/date.ts export const formatDate = (d: Date): string => d.toISOString();

Коли formatDate змінюється, Turborepo виявляє зміну, пропускає apps/api (не зачеплений) і перебудовує тільки apps/web. Cache hit для api економить 40-60 секунд на типовій збірці. Без npm publish, без бампу версій, без координації.

Координація в polyrepo через semantic versioning

Та ж зміна formatDate в polyrepo виглядає так:

bash
# В репо my-company-ui git commit -m "fix: formatDate returns ISO string" npm version patch # бамп до 1.0.1 npm publish # публікація в npm registry # В репо my-company-web (окремий PR, окремий CI-запуск) npm install @my-company/shared-utils@1.0.1 git commit -m "chore: bump shared-utils to 1.0.1"

Два репо, два PR, два CI-запуски. Якщо web і mobile обидва споживають shared-utils, це три скоординовані зміни заради одного виправлення. Цей coordination cost зростає лінійно з кількістю споживачів.

Контроль доступу в monorepo через CODEOWNERS

# .github/CODEOWNERS # UI-команда ревʼює всі зміни спільних компонентів /libs/shared-ui/ @ui-team # Бекенд-команда ревʼює API /apps/api/ @backend-team # Platform-команда ревʼює інфраструктуру /infra/ @platform-team

PR, що торкається libs/shared-ui, автоматично запитує ревʼю від @ui-team. Ніхто поза командою не може замерджити ці зміни без ревʼю. Модель більш гранулярна, ніж дозволи на рівні репо в polyrepo. Побоювання що "monorepo = всі бачать все і можуть все" приходить від команд, які просто не налаштували CODEOWNERS.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?