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

  1. Start with shared libraries: Move ui-components and utils into a monorepo first
  2. Add one app at a time: Move the frontend, verify CI works, then move the backend
  3. Preserve Git history: Use git subtree or tools like tomono to merge repos with history
  4. Set up affected commands early: Without Nx/Turborepo, CI will be painfully slow
  5. 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.