The monorepo vs polyrepo debate is one of the longest-running arguments in software engineering. Monorepo advocates point to Google, Meta, and Microsoft. Polyrepo advocates point to Netflix, Amazon, and Spotify. The truth is that both approaches work — for different organizational structures and different trade-offs.
What Is a Monorepo?
A monorepo stores all projects, services, and libraries in a single Git repository. This does not mean a monolith — the code is still modular, but lives in one repository with shared tooling.
# Monorepo structure
mycompany/
apps/
web/ # Frontend application
api/ # Backend API
mobile/ # Mobile app
admin/ # Admin dashboard
packages/
ui-components/ # Shared design system
auth/ # Shared authentication library
utils/ # Common utilities
api-client/ # Generated API client
tools/
eslint-config/ # Shared ESLint config
tsconfig/ # Shared TypeScript config
package.json # Root workspace config
nx.json # Nx configuration
turbo.json # Or Turborepo configuration
What Is a Polyrepo?
Each project, service, or library gets its own Git repository. Teams have full autonomy over their repository, tooling, and deployment.
# Polyrepo structure (each is a separate Git repo)
mycompany/web # Frontend app repo
mycompany/api # Backend API repo
mycompany/mobile # Mobile app repo
mycompany/ui-lib # Design system repo (published to npm)
mycompany/auth-lib # Auth library repo (published to npm)
mycompany/infra # Infrastructure repo
Trade-offs Comparison
| Factor | Monorepo | Polyrepo |
|---|---|---|
| Code sharing | Easy (import directly) | Hard (publish packages, manage versions) |
| Atomic changes | One PR changes API + frontend | Separate PRs, coordinate releases |
| CI/CD speed | Slow without smart caching | Fast (only builds one project) |
| Team autonomy | Lower (shared config, shared CI) | Higher (own tools, own processes) |
| Dependency management | Single version per dependency | Each repo can use different versions |
| Onboarding | Clone once, see everything | Must find and clone multiple repos |
| Git performance | Degrades with size (millions of files) | Always fast (small repos) |
| Code visibility | Everyone sees everything | Scoped to team repos |
Monorepo Tooling: Nx
# Initialize an Nx workspace
npx create-nx-workspace@latest mycompany --preset=ts
# Project structure with Nx
# Nx provides:
# - Dependency graph (knows which projects depend on which)
# - Affected commands (only test/build what changed)
# - Computation caching (never rebuild the same code twice)
# - Task orchestration (parallel builds respecting dependencies)
# Only build projects affected by your changes
nx affected --target=build
# Only test what changed (not everything)
nx affected --target=test
# View the dependency graph
nx graph
# Opens a visual graph showing project dependencies
# Cache: if the inputs haven't changed, return cached output
nx build web
# First run: 45 seconds
# Second run: 0.1 seconds (cache hit!)
# Remote caching: share cache across team and CI
# nx.json
{
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"accessToken": "your-nx-cloud-token"
}
}
}
}
Monorepo Tooling: Turborepo
# turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"], // Build dependencies first
"outputs": ["dist/**"] // Cache these outputs
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"deploy": {
"dependsOn": ["build", "test", "lint"],
"outputs": []
}
}
}
# Run builds in parallel, respecting dependency order
turbo run build
# Only run affected tasks
turbo run build --filter=...[origin/main]
# Remote caching with Vercel
turbo run build --remote-only
Managing Dependencies
# npm/pnpm workspaces: shared dependencies at root
# package.json (root)
{
"workspaces": [
"apps/*",
"packages/*"
]
}
# pnpm-workspace.yaml (pnpm - recommended for monorepos)
packages:
- 'apps/*'
- 'packages/*'
# Internal packages: reference directly
# apps/web/package.json
{
"dependencies": {
"@mycompany/ui-components": "workspace:*",
"@mycompany/auth": "workspace:*"
}
}
# No publishing needed! pnpm resolves workspace: to local paths
# Changes to ui-components are immediately available in web
CI/CD for Monorepos
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for affected detection
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
# Only lint, test, build what changed
- run: npx nx affected --target=lint --base=origin/main
- run: npx nx affected --target=test --base=origin/main
- run: npx nx affected --target=build --base=origin/main
# Key: fetch-depth: 0 lets Nx compare against main
# to determine which projects are affected by your changes
When to Choose Monorepo
- Shared code between projects: Design system, utilities, API clients used by multiple apps
- Atomic cross-project changes: API change + frontend update in one PR
- Consistent tooling: Same linting, testing, and build configuration everywhere
- Small to medium team (2-30 devs): Everyone works on related code
- Full-stack features: One developer changes frontend + backend together
When to Choose Polyrepo
- Autonomous teams: Each team owns their deployment pipeline end-to-end
- Different tech stacks: One team uses Python, another Go, another Java
- Strong service boundaries: Services interact only through APIs, no shared code
- Large organization (100+ devs): Too many people for effective shared tooling
- Open source project: External contributors should not see internal services
Migration Strategy: Polyrepo to Monorepo
- Start with shared libraries: Move ui-components and utils into a monorepo first
- Add one app at a time: Move the frontend, verify CI works, then move the backend
- Preserve Git history: Use
git subtreeor tools like tomono to merge repos with history - Set up affected commands early: Without Nx/Turborepo, CI will be painfully slow
- Keep deployment independent: Moving code to a monorepo does not mean coupling deployments
# Merge a repo into monorepo preserving history
# In the monorepo:
git remote add web-repo https://github.com/mycompany/web.git
git fetch web-repo
git merge web-repo/main --allow-unrelated-histories
# Move files to the right directory
git mv src/ apps/web/src/
git mv package.json apps/web/package.json
git commit -m "chore: migrate web app into monorepo"
Key Takeaways
- Monorepo is not a monolith — code is modular, but lives in one repository
- Smart caching makes monorepos fast — without Nx or Turborepo, CI crawls at scale
- Polyrepo gives team autonomy at the cost of harder code sharing and cross-project changes
- Monorepo gives consistency at the cost of shared tooling complexity and Git performance
- Use pnpm workspaces for dependency management in JavaScript/TypeScript monorepos
- affected commands are essential — only build and test what changed, not everything
- Match the structure to your organization: one team = monorepo; many autonomous teams = polyrepo
- You can start monorepo and split later (or vice versa) — neither choice is permanent
The monorepo vs polyrepo decision is fundamentally about your organization, not your code. If your teams share code and coordinate releases, a monorepo reduces friction. If your teams are autonomous and deploy independently, polyrepo gives them freedom. Choose the structure that matches how your teams actually work, not how you wish they worked.