Skip to content

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.

3 min read
Repository structure comparison showing monorepo with shared packages versus separate polyrepo repositories

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.

tstypescript
// ❌ 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 review

Shared configuration

jsonjson
// 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

ymlyaml
# ❌ 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 service

Team 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.

jsonjson
// turbo.json — Turborepo task pipeline
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {},
    "typecheck": {
      "dependsOn": ["^build"]
    }
  }
}
shbash
# 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

FactorMonorepoPolyrepo
Team size< 50 engineers> 50 or multiple autonomous teams
Shared codeHeavy sharing across projectsLittle cross-project sharing
Deploy cadenceSimilar across projectsVery different per team
Tech stackMostly homogeneousDiverse (different languages)
Tooling investmentWilling to learn Turborepo/NxWant 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

tstypescript
// ❌ 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-types

In 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

  1. Monorepos enable atomic changes across shared libraries and all their consumers
  2. Polyrepos give teams autonomy over tooling, CI, and deployment cadence
  3. Monorepos require build tool investment — without Turborepo or Nx, CI times become unbearable
  4. Polyrepos require registry infrastructure for sharing libraries across repositories
  5. Choose based on team structure and code sharing patterns, not trends
Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX