Monorepo vs Polyrepo: How to Structure Your Codebase at Scale

Google uses a monorepo with 2 billion lines of code. Netflix uses hundreds of separate repos. Both work. Learn when each approach wins, the tooling that makes monorepos viable (Nx, Turborepo), and how to migrate without losing your mind.

Monorepo vs Polyrepo: How to Structure Your Codebase at Scale illustration
On this page11 sections

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.

Share this article

Stuck on implementation?

Get private, 1-on-1 help with system design, performance, scaling, or any technical challenge.

Book a Session

Related Production Resources

Course

Free learning tracks

Turn this guide into a structured production engineering path.

Lab

Interactive engineering labs

Practice the same ideas through scenario-based simulators.

Reference

Production cheatsheets

Keep the operational commands and checks nearby.

Glossary

Key terms

Review the vocabulary behind the architecture.

Discussion

Questions, corrections, or production notes? Add them here so other learners can benefit.

Continue Reading

Related practical guides from the same production engineering path.

Backend 20 min read

Caching Strategies: Cache-Aside, Write-Through, Distributed Caches, and Invalidation in Production

Every cache eventually causes an outage if you do not design it right. Cache-aside vs write-through, distributed caching with Redis and Memcached, CDN edge caching, the thundering herd, hot keys, and the invalidation strategies that hold up at scale.

Caching Redis
DevOps 13 min read

GitHub Actions Mastery: CI/CD Pipelines That Actually Scale

Your GitHub Actions workflow takes 20 minutes and fails randomly. Learn matrix builds, reusable workflows, aggressive caching, secrets management, self-hosted runners, and monorepo strategies that cut build times by 80%.

GitHub Actions CI/CD