Monorepo vs Polyrepo - pros and cons?
Monorepo vs polyrepo - monorepo keeps all projects in one Git repository; polyrepo gives each project its own repo.
Theory
TL;DR
- Monorepo is one shared kitchen (same ingredients, same tools); polyrepo is separate kitchens per chef, with every spice duplicated
- Core difference: monorepo lets you ship a change to
shared-uiand update all consuming apps in one commit; polyrepo needs a publish cycle plus version bumps in each consumer - More than 5 interdependent packages, lean toward monorepo; independent teams owning separate services, lean toward polyrepo
- Main tools: Nx, Turborepo, Bazel for monorepo; Lerna and git submodules for polyrepo coordination
- Real numbers: Google runs one Bazel monorepo at 86TB; Netflix runs 700+ separate repos via Spinnaker
Quick example
Monorepo structure (Nx workspace):
my-company/
apps/
web/ # React app - imports shared-ui directly
api/ # Node.js backend
libs/
shared-ui/ # UI components, no publish step needed
nx.json # Builds only changed projectsPolyrepo equivalent:
my-company-web/ # Git repo #1 - imports shared-ui via npm
my-company-api/ # Git repo #2
my-company-ui/ # Git repo #3 - published to npm registryIn the monorepo, one git commit updates web and shared-ui together. In polyrepo, that same change needs three commits, an npm publish, and version bumps in every consumer.
Key difference
The real gap shows up when you change a shared library. In monorepo, you update shared-ui, and the dependency graph (Nx computes it from tsconfig.json paths) automatically flags web for rebuild. One PR, one review, zero version mismatch. In polyrepo, the same change requires publishing a new npm version, updating the version pin in every consumer, waiting for CI to pass in each repo separately, and coordinating merges across teams. Version drift is not theoretical - it happens on every non-trivial shared library update.
When to use
Choose monorepo when:
- Multiple apps share UI components, utilities, or types
- You need atomic changes across the stack (update an API contract and the frontend in one PR)
- Team is small to medium (under 50 developers) and owns the full product
- You are building a design system used by 3+ apps
Choose polyrepo when:
- Teams own fully independent services with no shared code
- You need strict access control at the repo level (compliance, security isolation)
- Services deploy on completely separate cadences with no coupling
- You are running microservices where each service is a black box
Comparison table
| Aspect | Monorepo | Polyrepo |
|---|---|---|
| Code sharing | Direct imports, no publishing | npm/package publish + version pinning |
| Commits | Atomic across all projects | Separate PRs, sync via tags/releases |
| Build speed | Cached/incremental (Turborepo caches 90%+) | Full rebuild per repo |
| Repo size | Grows to 100GB+ | Small (1-5GB each) |
| Access control | Path-based via CODEOWNERS | Repo-level permissions |
| CI/CD | Complex orchestration (Nx affected) | Simple per-repo pipelines |
| Tooling | Nx, Turborepo, Bazel | Lerna, git submodules |
| Who uses it | Google, Meta, Microsoft | Netflix, AWS |
| When to use | Shared code, unified product | Team autonomy, independent services |
How Git and build tools handle this
Git in monorepo treats the entire repository as one unit. A git diff spans all projects. Tools like Nx parse project.json and tsconfig.paths to build a dependency graph, then run nx affected:build to rebuild only what changed. Touch shared-ui, and Nx traces which apps import it and triggers only those builds. At Google scale, Bazel takes this further: it hashes every input (source file plus deps) and checks a remote cache first. With a 95% cache hit rate, building 100,000 files takes roughly the same time as building 1,000.
Polyrepo has no cross-repo awareness by default. CI runs independently per repo. When shared-ui publishes v2.1.0, nothing automatically notifies consuming repos. Teams discover breaking changes only when their own CI breaks after updating the version pin.
Common mistakes
Running full CI on every push in monorepo
Without affected filtering, a monorepo build runs all projects on every push. With 10 apps, that is a 10-minute CI pipeline for a one-line fix.
# Wrong: rebuilds everything
npm run build:all
# Right: rebuilds only affected projects and their dependents
npx nx affected:build --base=mainAssuming monorepo has no access control
This is wrong. GitHub and GitLab both support path-based branch protection. A CODEOWNERS file gives granular review requirements per directory:
# .github/CODEOWNERS
/libs/shared-ui/ @ui-team
/apps/api/ @backend-team
/infra/ @platform-teamPull requests touching libs/shared-ui automatically request review from @ui-team. The access model is more granular than polyrepo's repo-level permissions, not less.
Polyrepo without automated versioning
Manual version bumps cause broken deploys. If shared-ui publishes a major version but a downstream team misses the bump, their build breaks without warning. Fix: automate with conventional commits plus npm version and changelog generation in CI.
Migrating to monorepo by merging git histories
Merging full histories from 5 repos creates an unreadable git log with 50,000 commits from unrelated projects. Use git filter-repo to squash history, or import with a clean initial commit per project.
Ignoring remote caching
Monorepo without remote cache slows down as the codebase grows and eventually hits polyrepo speed. Turborepo's Vercel Remote Cache or Nx Cloud distribute cached artifacts across all developers and CI machines. One engineer's build populates the cache for the next ten.
Real-world usage
- Google uses Bazel on a single monorepo with 2 billion lines of code and 120,000 engineers; changes to Android and Chrome ship in one atomic commit
- Meta runs Buck on a monorepo containing React Native and all web apps
- Microsoft uses Nx for VS Code and TypeScript development
- Netflix operates 700+ separate repos managed via Spinnaker for deployment orchestration
- Uber uses a monorepo with 1,000+ packages for coordinating mobile and web platforms
Follow-up questions
Q: How does Nx determine which projects are affected by a change?
A: Nx parses project.json and tsconfig.paths to build a dependency graph. Running nx graph visualizes it. When you push, Nx compares the current commit to a base SHA and traces which projects import the changed files.
Q: What is the tradeoff between Bazel and Turborepo?
A: Bazel is hermetic: builds are reproducible across any machine because every input is hashed and isolated. Turborepo is much faster to set up and works well for JavaScript-only projects. Bazel makes sense at 100,000+ files; Turborepo covers most teams below that threshold.
Q: How do you enforce module boundaries in a monorepo?
A: Nx provides enforceModuleBoundaries in the eslint config. It blocks invalid imports between packages based on tags you assign to each project. For example, web can import shared-ui but not api.
Q: Can polyrepo teams share code without switching to monorepo?
A: Yes, via a private npm registry. Teams publish packages and consume them as versioned dependencies. The tradeoff is the publish-update-merge cycle on every shared change. Git submodules are an option but slow to update and prone to state inconsistencies.
Q: How do you scale a monorepo to thousands of developers?
A: Google answers this with Piper, an internal VCS handling 1 million commits per day, path-based ACLs, and Bazel remote execution (RBE) which distributes build steps across a cluster. For smaller scale, Nx Cloud or Turborepo remote caching cover the main bottleneck.
Examples
Sharing a utility with 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();When formatDate changes, Turborepo detects the change, skips apps/api (unaffected), and rebuilds only apps/web. The cache hit for api saves 40-60 seconds on a typical build. No npm publish, no version bump, no coordination.
Polyrepo coordination with semantic versioning
That same formatDate change in polyrepo looks like this:
# In my-company-ui repo
git commit -m "fix: formatDate returns ISO string"
npm version patch # bumps to 1.0.1
npm publish # pushes to npm registry
# In my-company-web repo (separate PR, separate CI run)
npm install @my-company/shared-utils@1.0.1
git commit -m "chore: bump shared-utils to 1.0.1"Two repos, two PRs, two CI runs. If web and mobile both consume shared-utils, that is three coordinated changes for one bug fix. This coordination cost grows linearly with the number of consumers.
Access control in monorepo with CODEOWNERS
# .github/CODEOWNERS
# UI team reviews all shared component changes
/libs/shared-ui/ @ui-team
# Backend team reviews API changes
/apps/api/ @backend-team
# Platform team reviews infrastructure
/infra/ @platform-teamA PR touching libs/shared-ui automatically requests review from @ui-team. Nobody outside that team can merge those changes unreviewed. The model is more fine-grained than polyrepo's repo-level permissions. The concern that "monorepo means no access control" comes from teams that never configure CODEOWNERS.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.