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
Порівняльна таблиця
| Аспект | Monorepo | Polyrepo |
|---|---|---|
| Шерінг коду | Прямі імпорти, без публікації | npm-публікація + version pinning |
| Коміти | Атомарні по всіх проектах | Окремі PR, синхронізація через теги |
| Швидкість збірки | Кешована/інкрементальна (Turborepo кешує 90%+) | Повна перебудова кожного репо |
| Розмір репо | Росте до 100GB+ | Невеликий (1-5GB кожне) |
| Контроль доступу | Шлях-базований через CODEOWNERS | Дозволи на рівні репо |
| CI/CD | Складна оркестрація (Nx affected) | Прості пайплайни на кожне репо |
| Інструменти | Nx, Turborepo, Bazel | Lerna, git submodules |
| Хто використовує | Google, Meta, Microsoft | Netflix, 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-пайплайн на однорядкове виправлення.
# Неправильно: перебудовує все
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-teamPull 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
// apps/web/package.json
{
"dependencies": {
"@my-company/shared-utils": "workspace:*"
}
}// apps/web/src/api.ts
import { formatDate } from '@my-company/shared-utils';
const display = formatDate(new Date()); // "2024-01-15T00:00:00.000Z"// 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 виглядає так:
# В репо 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-teamPR, що торкається libs/shared-ui, автоматично запитує ревʼю від @ui-team. Ніхто поза командою не може замерджити ці зміни без ревʼю. Модель більш гранулярна, ніж дозволи на рівні репо в polyrepo. Побоювання що "monorepo = всі бачать все і можуть все" приходить від команд, які просто не налаштували CODEOWNERS.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.