Introduction
The monorepo vs polyrepo debate is one of the most enduring architectural discussions in software engineering. A monorepo stores all code in a single repository, while a polyrepo uses separate repositories for each project or service. This decision has profound implications for code sharing, collaboration, CI/CD, dependency management, and team autonomy. Neither approach is universally superiorβeach has distinct advantages and trade-offs that make it better suited for different organizational contexts.
Understanding when to use each approach requires looking beyond the technical details to consider organizational factors like team size, culture, deployment independence, and code ownership. Companies like Google and Meta use monorepos at massive scale, while Amazon and Netflix successfully use polyrepos. This guide provides a comprehensive comparison to help you make the right choice for your team and project.
Understanding Monorepo and Polyrepo: Core Concepts
What is a Monorepo?
A monorepo is a single repository containing multiple projects, packages, or services. All code lives together with shared tooling, CI/CD, and dependency management. The concept has been around since the early days of version control, but modern tooling has made it practical for teams of all sizes. Google famously maintains billions of lines of code in a single monorepo, and companies like Meta, Twitter, and Uber have followed suit.
Monorepo Structure:
company-monorepo/
βββ apps/
β βββ web/ # Frontend application
β βββ api/ # Backend API
β βββ mobile/ # Mobile app
β βββ admin/ # Admin dashboard
βββ packages/
β βββ ui/ # Shared UI components
β βββ utils/ # Shared utilities
β βββ database/ # Database client
β βββ config/ # Shared configurations
βββ tools/
β βββ eslint-config/ # Shared ESLint config
β βββ tsconfig/ # Shared TypeScript config
βββ package.json
βββ turbo.json
βββ pnpm-workspace.yaml
The key characteristic of a monorepo is that every commit creates a single, atomic change across all affected packages. This eliminates the "diamond dependency" problem where two packages depend on different versions of a third package, because everything is always compatible at HEAD.
What is a Polyrepo?
A polyrepo is an architecture where each project, service, or library has its own repository with independent CI/CD, versioning, and deployment. Each repository is a self-contained unit with its own release cycle, issue tracker, and access controls.
Polyrepo Structure:
# Separate repositories
github.com/company/web-frontend
github.com/company/api-service
github.com/company/mobile-app
github.com/company/admin-dashboard
github.com/company/ui-library
github.com/company/utils-library
github.com/company/database-client
In a polyrepo architecture, shared code is distributed through package managers (npm, PyPI, Maven Central) or artifact registries. This creates a clear boundary between projects and enforces modular design through explicit dependency declarations.
Key Differences
| Aspect | Monorepo | Polyrepo |
|---|---|---|
| Repository Count | One | Many |
| Code Sharing | Direct imports | Published packages |
| Dependency Management | Workspace linking | Package registry |
| CI/CD | Single pipeline | Multiple pipelines |
| Versioning | Synchronized or independent | Independent |
| Code Ownership | Shared | Team-owned |
| Tooling | Centralized | Per-repo |
| Access Control | Coarse-grained | Fine-grained |
| Git History | Unified | Distributed |
| Atomic Changes | Yes | No |
Architecture and Design Patterns
Monorepo Architecture
A well-structured monorepo uses workspaces to organize code into logical groups. The workspace configuration defines how packages relate to each other and enables the build tool to understand the dependency graph:
// Root package.json
{
"name": "company-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*",
"tools/*"
],
"devDependencies": {
"turbo": "^1.10.0",
"typescript": "^5.0.0"
}
}
// pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'The dependency graph in a monorepo is typically managed through a tool like Turborepo or Nx. These tools understand which packages depend on which others, enabling smart build ordering and caching. When you change a leaf package, only the packages that depend on it need to be rebuilt.
// turbo.json - defining the build pipeline
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"deploy": {
"dependsOn": ["build", "test"],
"cache": false
}
}
}Polyrepo Architecture
Each repository has its own configuration, CI/CD pipeline, and deployment strategy. This creates independent units that can be developed, tested, and deployed without coordination:
// web-frontend/package.json
{
"name": "@company/web-frontend",
"version": "1.0.0",
"dependencies": {
"@company/ui-library": "^2.0.0",
"@company/utils-library": "^1.5.0"
}
}
// .github/workflows/ci.yml (in each repo)
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- run: npm testIn a polyrepo, shared libraries are published to a registry. This creates an explicit contract between the library and its consumers. Versioning follows semantic versioning (semver), where breaking changes require a major version bump.
# Publishing a shared library
cd ui-library
npm version minor # bump from 2.0.0 to 2.1.0
npm publish --access public
# Consuming the library
cd web-frontend
npm install @company/ui-library@^2.0.0Hybrid Approaches
Many organizations use a hybrid approach that combines the benefits of both architectures:
# Core shared code in monorepo
github.com/company/core-libraries/
βββ packages/
β βββ ui-components/
β βββ auth-utils/
β βββ api-client/
# Services in separate repos
github.com/company/user-service
github.com/company/payment-service
github.com/company/notification-service
A hybrid approach works well when you have a core set of libraries that are tightly coupled and frequently updated together, but also have services that are independently deployable and maintained by different teams. The key is to identify the boundaries between tightly-coupled and loosely-coupled code.
Step-by-Step Implementation
Setting Up a Monorepo with Turborepo
Turborepo is the most popular monorepo build system for JavaScript/TypeScript projects. It provides intelligent caching, parallel execution, and incremental builds:
# Create monorepo with Turborepo
npx create-turbo@latest my-monorepo
# Structure
my-monorepo/
βββ apps/
β βββ web/
β βββ api/
βββ packages/
β βββ ui/
β βββ utils/
β βββ database/
βββ turbo.json
βββ package.json
βββ pnpm-workspace.yaml
# Run all builds
pnpm turbo build
# Run affected builds only
pnpm turbo build --filter='...[origin/main]'
# Run with remote caching
pnpm turbo build --team=my-teamTurborepo's caching works by hashing inputs (source files, dependencies, environment variables) and storing the outputs. If the inputs haven't changed, the cached outputs are restored. This can reduce build times from minutes to seconds.
// Example: Using workspace packages in a monorepo
// apps/web/src/App.tsx
import { Button, Input, Modal } from '@company/ui';
import { formatDate, debounce } from '@company/utils';
import { db, prisma } from '@company/database';
export function App() {
return (
<div>
<Input placeholder="Search..." />
<Button onClick={handleSearch}>Search</Button>
<Modal isOpen={isOpen}>
<p>Results found on {formatDate(new Date())}</p>
</Modal>
</div>
);
}Setting Up a Polyrepo with Shared Libraries
In a polyrepo, shared code must be published and versioned independently. This creates a more formal contract between packages:
# Create separate repositories
gh repo create company/web-frontend --public
gh repo create company/api-service --public
gh repo create company/ui-library --public
# Each repo has independent CI/CD
# web-frontend/.github/workflows/ci.yml
# api-service/.github/workflows/ci.yml
# ui-library/.github/workflows/ci.yml
# Publish shared library to npm
cd ui-library
npm publish --access publicMigrating from Polyrepo to Monorepo
Migrating from a polyrepo to a monorepo is a significant undertaking that requires careful planning. The key challenge is preserving git history while combining repositories:
# 1. Create monorepo structure
mkdir company-monorepo
cd company-monorepo
git init
# 2. Add existing repos as subtrees
git subtree add --prefix=apps/web git@github.com:company/web-frontend.git main
git subtree add --prefix=apps/api git@github.com:company/api-service.git main
git subtree add --prefix=packages/ui git@github.com:company/ui-library.git main
# 3. Configure workspaces
# Update package.json and turbo.json
# 4. Update CI/CD
# Modify .github/workflows/ci.yml for monorepoAn alternative to git subtree is git filter-repo, which can rewrite history to move files into subdirectories. This preserves individual commit history more accurately but is more complex:
# Using git filter-repo (more precise history preservation)
cd web-frontend
git filter-repo --to-subdirectory-filter apps/web
cd ../company-monorepo
git remote add web-frontend ../web-frontend
git fetch web-frontend
git merge web-frontend/main --allow-unrelated-historiesReal-World Use Cases
Use Case 1: Startup with Small Team (Monorepo)
A startup with 5-15 engineers building a SaaS product benefits enormously from a monorepo. When the team is small, coordination overhead is minimal, and the ability to make atomic changes across the stack is invaluable:
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"]
},
"deploy": {
"dependsOn": ["build", "test"],
"cache": false
}
}
}
// GitHub Actions - deploy only changed apps
- name: Deploy
run: pnpm turbo deploy --filter='...[origin/main]'Benefits:
- Single CI/CD pipeline reduces DevOps overhead
- Easy code sharing through direct imports
- Atomic commits across packages prevent version mismatches
- Consistent tooling and configuration across all code
Use Case 2: Enterprise with Multiple Teams (Polyrepo)
An enterprise with 10+ teams, each owning a service, often benefits from polyrepo. Each team can choose their own tech stack, release cadence, and deployment strategy:
# Each team owns their repository
team-user/
βββ user-service/
βββ user-api/
βββ user-tests/
team-payment/
βββ payment-service/
βββ payment-api/
βββ payment-tests/
team-notification/
βββ notification-service/
βββ notification-api/
βββ notification-tests/
Benefits:
- Team autonomy over technology choices and release cadence
- Independent deployments without cross-team coordination
- Clear ownership boundaries with repository-level access control
- Technology flexibility (different teams can use different languages)
Use Case 3: Large-Scale Organization (Hybrid)
A large organization with shared libraries and independent services often adopts a hybrid approach. The core shared libraries live in a monorepo, while services are in separate repos:
// Shared libraries in monorepo
// core-libraries/package.json
{
"workspaces": [
"packages/*"
]
}
// Services in separate repos
// user-service/package.json
{
"dependencies": {
"@company/core-auth": "^1.0.0",
"@company/core-database": "^2.0.0",
"@company/core-logging": "^1.5.0"
}
}Use Case 4: Open Source Project with Plugins (Monorepo)
Open source projects with a core library and multiple plugins benefit from a monorepo. Babel, Jest, and Next.js all use this pattern:
babel-monorepo/
βββ packages/
β βββ babel-core/
β βββ babel-cli/
β βββ babel-preset-env/
β βββ babel-preset-react/
β βββ babel-plugin-transform-arrow-functions/
β βββ babel-plugin-transform-classes/
βββ package.json
βββ lerna.json
Best Practices for Production
-
Consider team structure: Use Conway's Law as a guideβyour repository structure should reflect your organizational structure. If your teams are organized by service, a polyrepo may be more natural.
-
Start with monorepo for new projects: Most new projects benefit from monorepo simplicity. Migrate to polyrepo when you need team autonomy. The reverse migration (polyrepo to monorepo) is significantly more complex.
-
Use remote caching: Enable Turborepo or Nx Cloud for remote caching to reduce CI times. Remote caching can reduce build times by 50-90% in CI environments where local caches are not persisted.
-
Implement affected detection: Never run full builds on PRs. Use affected detection to only build and test what changed. This is critical for keeping CI times manageable as the codebase grows.
-
Establish clear ownership: Use CODEOWNERS files to define who reviews what code. This prevents bottlenecks where a single team reviews all changes:
# CODEOWNERS
/apps/web/ @frontend-team
/apps/api/ @backend-team
/packages/ui/ @design-system-team
/packages/auth/ @security-team
-
Standardize tooling: Use shared ESLint, Prettier, and TypeScript configurations across all packages. This eliminates configuration drift and makes onboarding easier.
-
Automate publishing: Use Changesets for automated versioning and publishing. Changesets create a changelog entry with each PR and batch releases:
# Add a changeset
pnpm changeset
# Version packages
pnpm changeset version
# Publish
pnpm changeset publish- Monitor performance: Track build times, cache hit rates, and CI costs. Set up alerts for when build times exceed thresholds.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Monorepo with too many packages | Slow builds, complex dependencies | Split into smaller monorepos or hybrid approach |
| Polyrepo with too many repos | Coordination overhead, dependency hell | Consolidate related repos into a monorepo |
| No remote caching | Slow CI, redundant builds | Enable Turborepo or Nx Cloud |
| Missing affected detection | Wasted CI resources | Implement dependency-aware builds |
| Inconsistent tooling | Build failures, style inconsistencies | Use shared configs with a tools/ directory |
| No clear ownership | Code quality issues, slow reviews | Use CODEOWNERS files |
| Tight coupling across repos | Deployment coordination nightmares | Move tightly coupled code to same repo |
| Monorepo without code ownership | Any dev can change anything | Use CODEOWNERS and lint rules |
Performance Optimization
Monorepo Performance
Monorepo performance is heavily influenced by your build tool configuration. Here are key optimizations:
# Turborepo: Run with concurrency control
pnpm turbo build --concurrency=10
# Turborepo: Prune unused packages for Docker builds
pnpm turbo prune --scope=web --docker
# Turborepo: Check cache hit rate
pnpm turbo build --dry-run=json | jq '.tasks | map(select(.cache == "HIT")) | length'
# Turborepo: Profile build to find bottlenecks
pnpm turbo build --profile
# Open the generated .turbo/profile.json in chrome://tracingNx offers similar capabilities with additional features like computation caching and distributed task execution:
# Nx: Run affected targets
nx affected --target=build
# Nx: Visualize the dependency graph
nx graph
# Nx: Run tasks in parallel with distribution
npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js"Polyrepo Performance
# GitHub Actions: Cache dependencies
- uses: actions/cache@v3
with:
path: |
node_modules
~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# GitHub Actions: Matrix builds for multiple services
strategy:
matrix:
service: [user-service, payment-service, notification-service]
fail-fast: falseComparison with Alternatives
| Aspect | Monorepo | Polyrepo | Hybrid |
|---|---|---|---|
| Code Sharing | Easy (direct imports) | Hard (package publishing) | Medium |
| CI/CD | Single pipeline | Multiple pipelines | Mixed |
| Team Autonomy | Low | High | Medium |
| Dependency Management | Simple | Complex | Medium |
| Code Ownership | Shared | Clear | Mixed |
| Learning Curve | Medium | Low | High |
| Best For | Small-medium teams | Large enterprises | Mixed organizations |
| Git Performance | Can degrade at scale | Always fast | Varies |
| Rollback | Atomic across packages | Per-repo | Mixed |
Advanced Patterns and Techniques
Monorepo with Micro-Frontends
A monorepo is an excellent fit for micro-frontend architectures, where each team owns a piece of the UI:
// apps/shell/package.json
{
"dependencies": {
"@company/mfe-products": "workspace:*",
"@company/mfe-cart": "workspace:*",
"@company/mfe-checkout": "workspace:*"
}
}
// turbo.json
{
"pipeline": {
"build:mfe": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"deploy:mfe": {
"dependsOn": ["build:mfe"],
"cache": false
}
}
}Each micro-frontend is an independent deployable unit, but they all share the same design system and utilities through workspace dependencies.
Polyrepo with Shared CI Templates
Polyrepos can reduce CI duplication through reusable GitHub Actions workflows:
# .github/workflows/shared-ci.yml (reusable workflow)
name: Shared CI
on:
workflow_call:
inputs:
node-version:
required: true
type: string
deploy-environment:
required: false
type: string
default: 'staging'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm test
deploy:
needs: build
if: inputs.deploy-environment != ''
runs-on: ubuntu-latest
environment: ${{ inputs.deploy-environment }}
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npm run deploy
# In each service repo
# .github/workflows/ci.yml
jobs:
ci:
uses: company/shared-ci/.github/workflows/shared-ci.yml@main
with:
node-version: '20'
deploy-environment: 'staging'Git Submodules for Polyrepo
Git submodules can bridge separate repositories when you need to share specific components:
# Add shared library as submodule
git submodule add git@github.com:company/ui-library.git packages/ui
# Update submodule to latest
git submodule update --remote packages/ui
# Clone with submodules
git clone --recurse-submodules git@github.com:company/web-frontend.gitHowever, submodules have significant drawbacks: they require explicit updates, can cause confusion with detached HEAD states, and add complexity to the development workflow. Most teams prefer package registries over submodules.
Sparse Checkout for Large Monorepos
Git sparse checkout allows developers to work with a subset of a monorepo, reducing clone times and disk usage:
# Enable sparse checkout
git sparse-checkout init --cone
# Only checkout specific directories
git sparse-checkout set apps/web packages/ui
# Clone with sparse checkout
git clone --filter=blob:none --sparse git@github.com:company/monorepo.git
cd monorepo
git sparse-checkout set apps/web packages/uiThis is particularly useful for large monorepos where developers only work on a few packages. GitHub's monorepo tooling also supports this pattern through their enterprise offerings.
Testing Strategies
Testing in a monorepo requires careful consideration of what to test and when:
// Monorepo: Test affected packages
describe('Monorepo Affected Detection', () => {
test('detects affected packages correctly', async () => {
const affected = await getAffectedPackages('origin/main', 'HEAD');
expect(affected).toContain('web');
expect(affected).toContain('ui');
});
test('ignores unchanged packages', async () => {
const affected = await getAffectedPackages('origin/main', 'HEAD');
expect(affected).not.toContain('docs');
});
});
// Polyrepo: Test shared library integration
describe('Shared Library Integration', () => {
test('ui-library renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
test('utils-library works as expected', () => {
expect(formatDate(new Date('2023-01-01'))).toBe('Jan 1, 2023');
});
});In a monorepo, you can use workspace dependencies to test against the latest version of shared packages. In a polyrepo, integration tests must pin to specific published versions, which can mask breaking changes.
Code Review and Collaboration
Monorepos and polyrepos have different code review dynamics. In a monorepo, developers can see and review changes across all packages in a single pull request, which improves visibility but can lead to large, hard-to-review PRs. Use CODEOWNERS files to automatically assign reviewers based on which packages are affected. In a polyrepo, each repository has its own review process, which keeps PRs focused but makes cross-cutting changes harder to coordinate. Establish review guidelines that account for your repository structure, including when to request reviews from teams that own related packages.
Tooling Ecosystem
The tooling ecosystem for monorepos has matured significantly over the past few years. Here's a comparison of the major tools:
Turborepo is the most popular choice for JavaScript/TypeScript monorepos. It focuses on build orchestration and caching with minimal configuration. It's backed by Vercel and integrates well with their deployment platform.
Nx is a more comprehensive monorepo toolkit that includes build orchestration, code generation, dependency graph visualization, and distributed task execution. It's backed by Nrwl and supports multiple languages beyond JavaScript.
Bazel is Google's build system, designed for massive monorepos with multiple languages. It has a steep learning curve but offers the best performance for very large codebases. Companies like Uber, Stripe, and Dropbox use Bazel.
Pants is similar to Bazel but with a focus on Python and better developer ergonomics. It's used by companies like Twitter and Foursquare.
For polyrepos, tools like Bit and Lerna (in independent mode) help manage cross-repository dependencies and coordinated releases. Evaluate the available tooling when making your repository structure decision, as poor tooling support can negate the theoretical benefits of either approach.
Migration Strategies and Decision Framework
When to Migrate from Polyrepo to Monorepo
Consider migrating to a monorepo when:
- You frequently need to make coordinated changes across multiple repos
- Version compatibility issues are causing production incidents
- Onboarding new developers takes too long due to repository proliferation
- CI/CD pipelines are duplicating work across repos
When to Migrate from Monorepo to Polyrepo
Consider migrating to a polyrepo when:
- Build times have become unmanageable despite optimization
- Teams are blocked waiting for reviews on unrelated code
- Access control needs are more granular than what CODEOWNERS provides
- Teams need to use different technology stacks
Decision Matrix
Ask these questions to guide your decision:
- How many developers will work in the codebase? (< 50: monorepo, > 200: consider polyrepo)
- How often do changes span multiple packages? (frequently: monorepo, rarely: polyrepo)
- Do teams need independent deployment? (yes: polyrepo or hybrid, no: monorepo)
- What's your CI/CD infrastructure? (cloud-based: monorepo with remote caching, on-premise: consider polyrepo)
- What languages are in use? (single ecosystem: monorepo, multiple languages: consider Bazel or polyrepo)
Future Outlook
The monorepo vs polyrepo debate will continue as tools evolve. Turborepo and Nx are making monorepos more manageable for larger organizations by solving the caching and build performance challenges. Git improvements like sparse checkouts, partial clones, and the Scalar project are making large repositories more practical at scale. The trend toward microservices and micro-frontends favors polyrepo architectures, while the need for code sharing, consistency, and atomic changes favors monorepos.
The hybrid approach is becoming increasingly popular, combining shared libraries in a monorepo with independent services in separate repositories. This balances code sharing with team autonomy. New tools like Moon and Lage are also entering the monorepo build space, providing additional options for teams with specific needs.
The key insight is that your repository structure should evolve with your organization. What works for a 10-person startup may not work for a 500-person engineering organization. Plan for the future but don't over-engineer from day oneβstart simple and migrate when the pain points become clear.
Conclusion
The choice between monorepo and polyrepo depends on your organizational context, team structure, and technical requirements. There's no one-size-fits-all answer.
Key takeaways:
- Monorepo excels for small-medium teams needing code sharing and consistency
- Polyrepo excels for large organizations needing team autonomy and independence
- Hybrid approaches balance code sharing with team autonomy
- Consider Conway's Law - match your repo structure to your org structure
- Start simple - begin with monorepo, migrate to polyrepo if needed
- Use modern tooling - Turborepo, Nx, and Changesets make monorepos manageable
- Measure before migrating - quantify the pain points before committing to a structural change
Start by assessing your team structure, deployment requirements, and code sharing needs. If in doubt, start with a monorepoβit's easier to split a monorepo into polyrepos than to merge polyrepos into a monorepo.