Monorepo vs. Polyrepo: Real Trade-offs
The monorepo debate isn't about tools — it's about how your team communicates, deploys, and shares code across project boundaries.

Every growing engineering org eventually faces the monorepo question. One large repository for everything, or many small repositories — one per service or library? The answer isn't universal, and adopting either approach without understanding the trade-offs leads to pain.
What Each Approach Actually Means
A monorepo stores multiple projects in a single repository. A polyrepo gives each project its own repository. The distinction matters for dependency management, CI pipeline design, and cross-team collaboration.
# Monorepo structure
my-company/
├── apps/
│ ├── web/ # Next.js frontend
│ ├── api/ # Express backend
│ └── admin/ # Admin dashboard
├── packages/
│ ├── ui/ # Shared component library
│ ├── utils/ # Shared utilities
│ └── config/ # Shared configs (ESLint, TS)
├── package.json
└── turbo.json
# Polyrepo structure
my-company-web/ # Own repo, own CI, own deps
my-company-api/ # Own repo, own CI, own deps
my-company-admin/ # Own repo, own CI, own deps
my-company-ui-lib/ # Published to npm registry
my-company-utils/ # Published to npm registry
Where Monorepos Win
Atomic cross-project changes
When a shared library changes its API, the monorepo lets you update all consumers in a single commit.
// ❌ Polyrepo: changing a shared library's API requires coordinated releases
// 1. Update ui-lib, bump version, publish to npm
// 2. Update web app, install new version, fix breaking changes
// 3. Update admin app, install new version, fix breaking changes
// 4. Hope nothing breaks between steps 1 and 3
// ✅ Monorepo: one PR updates the library and all consumers
// packages/ui/src/Button.tsx — change the prop interface
// apps/web/src/pages/Home.tsx — update usage
// apps/admin/src/pages/Dashboard.tsx — update usage
// All in one commit, one CI run, one reviewShared configuration
// Monorepo root package.json
{
"private": true,
"workspaces": ["apps/*", "packages/*"],
"devDependencies": {
"typescript": "^5.3.0",
"eslint": "^8.56.0",
"prettier": "^3.2.0"
}
}One version of TypeScript, one ESLint config, one Prettier config. No drift between projects.
Code discovery
In a monorepo, searching for all usages of a function works with a single grep. In a polyrepo, you need to search across multiple repositories — and you might not know which ones use a given package.
Where Polyrepos Win
Independent deployment and scaling
# ❌ Monorepo CI — any change triggers checks for everything
# (unless you invest heavily in affected-project detection)
on:
push:
branches: [main]
jobs:
test-all:
runs-on: ubuntu-latest
steps:
- run: npm test --workspaces # Tests everything
# ✅ Polyrepo CI — only the changed service is tested and deployed
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test # Tests only this service
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: npm run deploy # Deploys only this serviceTeam autonomy
Polyrepos give teams full control over their technology choices, CI pipelines, and release cadences. Team A can use Vitest while Team B uses Jest. Team C can deploy hourly while Team D deploys weekly.
Simpler tooling
Monorepos require specialized tools (Turborepo, Nx, Lerna, Bazel) to handle incremental builds, task caching, and affected-project detection. Polyrepos work with standard tooling out of the box.
The Build Tool Tax
Monorepos only work well with proper build orchestration. Without it, CI times grow linearly with codebase size.
// turbo.json — Turborepo task pipeline
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {},
"typecheck": {
"dependsOn": ["^build"]
}
}
}# Only build and test projects affected by the current changes
turbo run build test --filter=...[HEAD~1]Without caching and affected-project detection, a monorepo with 20 packages becomes painfully slow. This tooling investment is the real cost of monorepos.
Decision Framework
| Factor | Monorepo | Polyrepo |
|---|---|---|
| Team size | < 50 engineers | > 50 or multiple autonomous teams |
| Shared code | Heavy sharing across projects | Little cross-project sharing |
| Deploy cadence | Similar across projects | Very different per team |
| Tech stack | Mostly homogeneous | Diverse (different languages) |
| Tooling investment | Willing to learn Turborepo/Nx | Want standard Git + CI |
The hybrid approach is also valid: a monorepo for closely related services that share code, with separate repos for independent systems.
Common Mistakes
// ❌ Monorepo anti-pattern: circular dependencies
// packages/auth imports from packages/user
// packages/user imports from packages/auth
// Build order becomes impossible to resolve
// ✅ Extract shared types into a separate package
// packages/shared-types — no dependencies on other packages
// packages/auth — depends on shared-types
// packages/user — depends on shared-typesIn polyrepos, the equivalent mistake is tightly coupling services through shared database access or synchronous API chains that could have been a single service.
Key Takeaways
- Monorepos enable atomic changes across shared libraries and all their consumers
- Polyrepos give teams autonomy over tooling, CI, and deployment cadence
- Monorepos require build tool investment — without Turborepo or Nx, CI times become unbearable
- Polyrepos require registry infrastructure for sharing libraries across repositories
- Choose based on team structure and code sharing patterns, not trends


