Home CS Fundamentals Monorepo vs Polyrepo: Which Should You Choose and Why?

Monorepo vs Polyrepo: Which Should You Choose and Why?

In Plain English 🔥
Imagine your family keeps all their documents — tax returns, school reports, recipes, car manuals — in one big shared filing cabinet in the hallway. That's a monorepo. A polyrepo is like every family member having their own personal filing cabinet locked in their bedroom. The shared cabinet means everyone can find anything fast, but it gets messy if nobody agrees on the filing system. The private cabinets are tidy per person, but good luck when Dad needs Mum's car insurance number at 11pm.
⚡ Quick Answer
Imagine your family keeps all their documents — tax returns, school reports, recipes, car manuals — in one big shared filing cabinet in the hallway. That's a monorepo. A polyrepo is like every family member having their own personal filing cabinet locked in their bedroom. The shared cabinet means everyone can find anything fast, but it gets messy if nobody agrees on the filing system. The private cabinets are tidy per person, but good luck when Dad needs Mum's car insurance number at 11pm.

Every software team, whether they know it or not, has already made one of the most consequential architectural decisions in their engineering culture: where does the code live? It sounds boring — it's just folder structure, right? Wrong. How you organize your repositories shapes your deployment pipeline, your team autonomy, your code-sharing patterns, your CI/CD costs, and even how quickly a new engineer can ship their first feature. It's a decision that compounds over years, and switching later is genuinely painful.

The problem this choice solves is coordination. Code doesn't live in isolation — your frontend calls your API, your API shares validation logic with your mobile app, your shared design tokens live somewhere. The question is whether you keep all of that in one unified repository (monorepo) or split each concern into its own separate repository (polyrepo). Both approaches work. Both have serious trade-offs. The dangerous move is picking one without understanding why.

By the end of this article you'll be able to explain the real architectural difference between monorepos and polyrepos, understand which companies use which and why, reason through the right choice for a given team size and product shape, and walk into a system design interview ready to defend your answer with specifics. No fluff — just the mental model you actually need.

What Monorepo and Polyrepo Actually Mean (Beyond the Buzzwords)

A monorepo (monolithic repository) is a single version-controlled repository that contains the source code for multiple — sometimes all — distinct projects, services, and libraries in an organization. Google's monorepo contains over 2 billion lines of code across thousands of projects. Meta, Twitter, and Airbnb all use variants of this approach. The key word is 'distinct' — we're not talking about a single giant application, we're talking about genuinely separate concerns (frontend, backend, mobile, shared libraries) all living under one roof.

A polyrepo (multi-repo) strategy means each service, application, or library gets its own dedicated repository with its own versioning, CI pipeline, access controls, and release cadence. Netflix, Amazon, and most microservices-heavy organizations lean this way. Each team owns their repo completely — they merge when they want, deploy when they want, and don't need to coordinate a monorepo-wide lint run.

Here's the nuance most articles miss: the choice isn't really about files and folders. It's about where you want your coordination overhead to live. Monorepos centralize coordination into tooling. Polyrepos distribute coordination into process and team communication. Neither eliminates the coordination — they just move it somewhere different.

repo_structure_comparison.sh · BASH
12345678910111213141516171819202122232425262728293031323334353637383940
# ─────────────────────────────────────────────
# MONOREPO STRUCTURE — everything under one roof
# ─────────────────────────────────────────────
# acme-platform/                  <── single Git repository
#   apps/
#     customer-web/               <── React frontend
#     admin-dashboard/            <── Internal tools UI
#     mobile/                     <── React Native app
#   services/
#     auth-service/               <── Node.js authentication API
#     payments-service/           <── Python billing logic
#     notifications-service/      <── Go push notification worker
#   packages/
#     ui-components/              <── shared design system
#     validation-schemas/         <── shared Zod/Yup schemas
#     api-client/                 <── auto-generated API client
#   tools/
#     eslint-config/              <── shared lint rules
#     tsconfig/                   <── shared TypeScript configs
#   package.json                  <── workspace root
#   nx.json                       <── build orchestrator config

echo "One git clone. Everything available. One PR can touch frontend + backend + shared lib."
echo ""

# ─────────────────────────────────────────────
# POLYREPO STRUCTURE — each project its own repo
# ─────────────────────────────────────────────
# github.com/acme/customer-web         <── owns its own git history
# github.com/acme/admin-dashboard      <── independent release cycle
# github.com/acme/mobile               <── separate CI/CD pipeline
# github.com/acme/auth-service         <── team A owns this
# github.com/acme/payments-service     <── team B owns this
# github.com/acme/notifications-service
# github.com/acme/ui-components        <── published to npm as @acme/ui
# github.com/acme/validation-schemas   <── published to npm as @acme/validation
# github.com/acme/api-client           <── published to npm as @acme/api-client

echo "Nine separate clones. Shared code versioned via package manager (npm, pip, maven)."
echo "A change to ui-components requires: bump version → publish → update dependents → re-deploy."
▶ Output
One git clone. Everything available. One PR can touch frontend + backend + shared lib.

Nine separate clones. Shared code versioned via package manager (npm, pip, maven).
A change to ui-components requires: bump version → publish → update dependents → re-deploy.
🔥
Key Mental Model:In a monorepo, sharing code is an import statement. In a polyrepo, sharing code is a release process. That one sentence explains 80% of why teams switch from one to the other.

The Real Trade-offs: What Nobody Tells You Until It's Too Late

The monorepo's killer feature is atomic commits. You can change a shared validation schema and update every service that uses it in a single pull request. The reviewer sees the full blast radius. The CI run tells you instantly if anything broke. This is incredibly powerful — especially in the early days of a product when APIs are unstable and shared contracts change weekly.

But monorepos have a dirty secret: they require serious tooling investment to stay healthy. A naive monorepo — just a regular Git repo with multiple project folders — will destroy your CI pipeline within months. Running every test suite on every commit is catastrophically slow. You need build orchestrators like Nx, Turborepo, or Bazel that understand the dependency graph and only rebuild what actually changed. That's non-trivial infrastructure work.

Polyrepos feel liberating at first. Teams move fast, own their destiny, and deploy independently. But the pain shows up in the seams. When you need to make a breaking change to a shared library, you're now coordinating a multi-repo migration — opening PRs across 12 repositories, chasing teams to upgrade, maintaining backwards-compatible shims for months. This is called 'dependency hell' and it's a very real operational cost that polyrepo advocates tend to understate.

The honest answer is: monorepos win on code consistency and atomic changes; polyrepos win on team autonomy and blast-radius isolation. Your job is figuring out which pain you'd rather deal with.

monorepo_ci_affected_only.yml · YAML
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
# ─────────────────────────────────────────────────────────────
# GitHub Actions + Nx: Smart CI that only runs affected projects
# This is the tooling investment a healthy monorepo requires.
# Without this, every commit runs ALL tests — unusable at scale.
# ─────────────────────────────────────────────────────────────

name: Affected Projects CI

on:
  pull_request:
    branches: [main]

jobs:
  setup-affected:
    runs-on: ubuntu-latest
    outputs:
      # Nx computes which projects are affected by changed files
      affected-apps: ${{ steps.nx-affected.outputs.apps }}
      affected-libs: ${{ steps.nx-affected.outputs.libs }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # full history needed for Nx affected comparison

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci

      - name: Compute affected projects
        id: nx-affected
        run: |
          # Nx compares current branch to main and builds a dependency graph.
          # Only projects whose source files (or dependencies) changed are returned.
          AFFECTED_APPS=$(npx nx print-affected --type=app --select=projects --base=origin/main)
          AFFECTED_LIBS=$(npx nx print-affected --type=lib --select=projects --base=origin/main)
          echo "apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT
          echo "libs=$AFFECTED_LIBS" >> $GITHUB_OUTPUT

  test-affected:
    needs: setup-affected
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci

      - name: Run tests only for affected projects
        run: |
          # If nothing is affected, this exits cleanly — no wasted CI minutes.
          # If payments-service changed, only payments-service and its dependents test.
          npx nx affected --target=test --base=origin/main --parallel=3

      - name: Run linting only for affected projects
        run: npx nx affected --target=lint --base=origin/main --parallel=3

      - name: Build affected projects (validates nothing is broken)
        run: npx nx affected --target=build --base=origin/main --parallel=3
▶ Output
# Example: PR only changes packages/validation-schemas/src/user.schema.ts

> nx print-affected --type=app --select=projects --base=origin/main
customer-web, admin-dashboard, auth-service

# Only 3 of 9 projects run their full test + lint + build.
# The other 6 are skipped entirely — CI completes in ~4 min instead of ~22 min.

✓ customer-web: test passed (1m 12s)
✓ admin-dashboard: test passed (0m 48s)
✓ auth-service: test passed (2m 03s)
✓ All affected checks passed.
⚠️
Watch Out:Starting a monorepo without a build orchestrator (Nx, Turborepo, or Bazel) is like building a skyscraper without an elevator. It works at two floors. It's a disaster at twenty. Budget the tooling setup time before committing to the monorepo path.

Sharing Code Across Projects: Monorepo Import vs Polyrepo Package Release

This is where the rubber meets the road. Let's say your team builds a formatCurrency utility used by your web frontend, your mobile app, and your invoicing service. How you share that function is fundamentally different between the two strategies — and the difference has serious implications for how quickly you can iterate.

In a monorepo, sharing is an internal import. No versioning, no publishing, no npm registry. You just import from a workspace package path. When you update formatCurrency, every consumer sees the change immediately — and your CI tells you instantly if any consumer broke. This is fast but requires discipline: a bad change to a shared utility can break many things at once.

In a polyrepo, that utility lives in its own repository, gets published to npm (or a private registry) as @acme/formatters, and each consumer pins a version. This is safer in one sense — consumers opt into upgrades — but it creates version fragmentation. Within months you'll have some services on v1.2.0, some on v2.0.0, and some on a fork. Keeping everyone current becomes a recurring operational burden. Tools like Renovate Bot help automate dependency PRs, but they don't eliminate the coordination cost.

shared_currency_formatter.ts · TYPESCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// ─────────────────────────────────────────────────────────────
// packages/formatters/src/currency.ts
// This shared utility lives ONCE — used by web, mobile, and API
// ─────────────────────────────────────────────────────────────

export interface CurrencyFormatOptions {
  locale: string;       // e.g. 'en-US', 'de-DE', 'ja-JP'
  currency: string;     // ISO 4217 code e.g. 'USD', 'EUR', 'GBP'
  showSymbol?: boolean; // default true
}

/**
 * Formats a numeric amount as a locale-aware currency string.
 * Centralising this prevents subtle regional formatting bugs
 * (e.g. de-DE uses comma as decimal separator, not period).
 */
export function formatCurrency(
  amountInCents: number,
  options: CurrencyFormatOptions
): string {
  const { locale, currency, showSymbol = true } = options;

  // Convert cents to major currency unit — avoids floating point errors
  // from storing dollars/pounds directly (e.g. 0.1 + 0.2 !== 0.3 in JS)
  const amountInMajorUnit = amountInCents / 100;

  return new Intl.NumberFormat(locale, {
    style: showSymbol ? 'currency' : 'decimal',
    currency,
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  }).format(amountInMajorUnit);
}

// ─────────────────────────────────────────────────────────────
// MONOREPO USAGE — apps/customer-web/src/components/PriceTag.tsx
// Direct workspace import. No version pinning. Always current.
// ─────────────────────────────────────────────────────────────

// tsconfig paths or package.json workspaces maps this automatically:
import { formatCurrency } from '@acme/formatters'; // resolves to ../../packages/formatters/src

const priceTag = formatCurrency(4999, { locale: 'en-US', currency: 'USD' });
const germanPrice = formatCurrency(4999, { locale: 'de-DE', currency: 'EUR' });
const japanesePrice = formatCurrency(4999, { locale: 'ja-JP', currency: 'JPY', showSymbol: false });

console.log('US price:', priceTag);
console.log('German price:', germanPrice);
console.log('Japanese price (no symbol):', japanesePrice);

// ─────────────────────────────────────────────────────────────
// POLYREPO EQUIVALENT — requires these steps first:
// 1. cd acme-formatters-repo && npm version patch && npm publish
// 2. cd customer-web-repo && npm install @acme/formatters@1.0.1
// 3. Same steps in mobile-repo and invoicing-service-repo
// Each repo now potentially pinned to a different version.
// ─────────────────────────────────────────────────────────────

// import { formatCurrency } from '@acme/formatters'; // ^1.0.1 pinned in package.json
▶ Output
US price: $49.99
German price: 49,99 €
Japanese price (no symbol): 49.99
⚠️
Pro Tip:In a monorepo, enforcing that internal packages are imported via their workspace alias (e.g. '@acme/formatters') rather than relative paths (e.g. '../../../packages/formatters/src') keeps your dependency graph clean and lets your build orchestrator correctly compute what's affected.

Choosing the Right Strategy: A Decision Framework That Actually Works

Forget the 'monorepos are for big companies' myth. The right choice depends on three factors: team topology, deployment coupling, and code sharing frequency.

If your teams regularly need to make cross-cutting changes — updating a shared API contract, rolling out a new auth pattern, migrating to a new logging standard — a monorepo will save you enormous coordination overhead. One PR, one review, one merge. Google uses a monorepo for exactly this reason: at their scale, fragmented repos would mean weeks of migration work for every platform-wide change.

If your teams are genuinely autonomous — they own their service end-to-end, rarely share code with other teams, and have completely independent deployment and on-call schedules — a polyrepo respects that boundary. Forcing them into a monorepo adds noise (other teams' CI failures blocking your merge, repository-wide conventions you didn't choose) without adding value.

The hybrid approach — sometimes called a 'multi-monorepo' — is increasingly common: one monorepo per domain or product area, with polyrepo boundaries between truly separate business units. Spotify does something like this. It's more complex to govern but avoids the extremes of both approaches.

Start with the question: 'How often do I need to change shared code and all its consumers simultaneously?' If the answer is 'constantly', lean monorepo. If the answer is 'rarely', lean polyrepo.

repo_strategy_decision_guide.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
/**
 * A practical decision framework for monorepo vs polyrepo.
 * Answer each question honestly — the scores will tell a story.
 *
 * Run this with: node repo_strategy_decision_guide.js
 */

const teamProfile = {
  // How many engineers will work across multiple services regularly?
  crossFunctionalEngineers: 8,          // e.g. fullstack devs who touch frontend + backend

  // How many distinct deployable services exist (or are planned)?
  numberOfServices: 6,

  // Rough % of code that is shared across 2+ services
  sharedCodePercentage: 35,             // e.g. auth utils, design system, validation schemas

  // Do teams deploy together (coordinated) or independently?
  deploymentCoupling: 'coordinated',    // 'coordinated' | 'independent'

  // Is there a dedicated platform/DevOps team to maintain CI tooling?
  hasDedicatedPlatformTeam: false,

  // Approximate team size
  totalEngineers: 25,
};

function recommendRepoStrategy(profile) {
  let monorepoScore = 0;
  let polyrepoScore  = 0;
  const reasoning = [];

  // Cross-functional engineers benefit massively from monorepo atomic commits
  if (profile.crossFunctionalEngineers > 5) {
    monorepoScore += 3;
    reasoning.push('✓ High cross-functional headcount → monorepo atomic PRs save coordination time');
  } else {
    polyrepoScore += 2;
    reasoning.push('✓ Low cross-functional work → polyrepo team boundaries are clean and respected');
  }

  // High shared code % makes polyrepo painful (version fragmentation)
  if (profile.sharedCodePercentage > 25) {
    monorepoScore += 3;
    reasoning.push('✓ High shared code (>25%) → polyrepo means constant version bumping and fragmentation risk');
  } else {
    polyrepoScore += 2;
    reasoning.push('✓ Low shared code → polyrepo versioning overhead is manageable');
  }

  // Coordinated deployments align naturally with monorepo PRs
  if (profile.deploymentCoupling === 'coordinated') {
    monorepoScore += 2;
    reasoning.push('✓ Coordinated deployments → monorepo keeps services in sync naturally');
  } else {
    polyrepoScore += 3;
    reasoning.push('✓ Independent deployments → polyrepo respects service autonomy and release cadence');
  }

  // Monorepo tooling investment needs someone to own it
  if (!profile.hasDedicatedPlatformTeam && profile.numberOfServices > 8) {
    polyrepoScore += 2;
    reasoning.push('⚠ No platform team + many services → monorepo CI tooling becomes a maintenance burden');
  } else if (profile.hasDedicatedPlatformTeam) {
    monorepoScore += 2;
    reasoning.push('✓ Platform team present → monorepo tooling (Nx/Turborepo) will be properly maintained');
  }

  // Small teams (<15 engineers) often find polyrepo overhead manageable
  if (profile.totalEngineers < 15) {
    polyrepoScore += 1;
    reasoning.push('✓ Small team → polyrepo simplicity outweighs monorepo tooling setup cost');
  } else if (profile.totalEngineers > 50) {
    monorepoScore += 2;
    reasoning.push('✓ Large team → monorepo discoverability and shared standards become critical at scale');
  }

  const recommendation = monorepoScore > polyrepoScore
    ? 'MONOREPO'
    : monorepoScore === polyrepoScore
      ? 'HYBRID (start polyrepo, merge into monorepo when shared code pain hits)'
      : 'POLYREPO';

  return { recommendation, monorepoScore, polyrepoScore, reasoning };
}

const result = recommendRepoStrategy(teamProfile);

console.log('═══════════════════════════════════════');
console.log('  REPO STRATEGY DECISION FRAMEWORK');
console.log('═══════════════════════════════════════');
console.log(`  Monorepo Score : ${result.monorepoScore}`);
console.log(`  Polyrepo Score : ${result.polyrepoScore}`);
console.log(`  Recommendation : ${result.recommendation}`);
console.log('───────────────────────────────────────');
console.log('  Reasoning:');
result.reasoning.forEach(reason => console.log(`  ${reason}`));
console.log('═══════════════════════════════════════');
▶ Output
═══════════════════════════════════════
REPO STRATEGY DECISION FRAMEWORK
═══════════════════════════════════════
Monorepo Score : 8
Polyrepo Score : 2
Recommendation : MONOREPO
───────────────────────────────────────
Reasoning:
✓ High cross-functional headcount → monorepo atomic PRs save coordination time
✓ High shared code (>25%) → polyrepo means constant version bumping and fragmentation risk
✓ Coordinated deployments → monorepo keeps services in sync naturally
✓ Platform team present → monorepo tooling (Nx/Turborepo) will be properly maintained
═══════════════════════════════════════
🔥
Interview Gold:When asked 'Which is better — monorepo or polyrepo?' the correct answer is 'It depends on deployment coupling and shared code frequency.' Then give an example of each scenario. That answer signals senior-level thinking. Saying 'monorepos are what Google uses so they must be better' signals junior-level thinking.
Feature / AspectMonorepoPolyrepo
Sharing code across servicesDirect import — no versioning neededMust publish to a package registry and version-pin
Atomic cross-service changesSingle PR touches all affected codeRequires coordinated PRs across multiple repos
CI/CD complexityHigh — needs affected-only build orchestration (Nx, Bazel)Low per-repo — each pipeline is simple and isolated
Tooling investment requiredSignificant upfront (Nx, Turborepo, or Bazel setup)Minimal — standard CI template per repo
Team autonomyReduced — all teams see and affect each other's PRsHigh — each team fully owns their repo and pipeline
Onboarding experienceExcellent — one clone, full context visibleFragmented — new devs clone 10+ repos to get started
Dependency version driftImpossible — one version of each dependency site-wideCommon — services pin different versions over time
Blast radius of a bad mergeHigher — can affect many projects at onceLower — a bad merge only affects one repo
Code discoverabilityHigh — all code searchable in one placeLow — requires knowing which repo to look in
Real-world examplesGoogle, Meta, Twitter, Airbnb, VercelNetflix, Amazon, most microservices-first orgs
Best suited forCo-deployed services with frequent shared code changesFully autonomous teams with independent release cycles

🎯 Key Takeaways

  • The choice between monorepo and polyrepo is really a question of where you want your coordination overhead — centralized in tooling (monorepo) or distributed across team processes (polyrepo). Neither eliminates the overhead.
  • A monorepo without a build orchestrator (Nx, Turborepo, Bazel) is unsustainable past a handful of projects — the 'affected-only' CI pattern is non-negotiable, not optional.
  • The clearest signal for choosing a monorepo is high shared-code percentage combined with coordinated deployments. The clearest signal for polyrepo is genuine team autonomy with independent release cadences.
  • Dependency version drift is polyrepo's hidden long-term tax — automate dependency updates with Renovate Bot from day one or you will spend a sprint every quarter chasing library upgrades across a dozen repos.

⚠ Common Mistakes to Avoid

  • Mistake 1: Treating a monorepo as a single deployable application — Symptom: teams run every test suite on every commit, CI takes 45+ minutes and developers start skipping it entirely — Fix: install Nx or Turborepo immediately, configure affected-only pipelines so only projects downstream of changed files are tested. A monorepo without a build graph tool isn't a monorepo strategy, it's just a big folder.
  • Mistake 2: Building a polyrepo without automating dependency updates — Symptom: within 6 months you have 8 services each pinned to a different version of your shared validation library, half of them have a security vulnerability that was patched in v2.1.0 — Fix: install Renovate Bot or Dependabot across all repos with auto-merge enabled for patch-level updates. Set a team policy that no service may lag more than one major version behind shared libraries.
  • Mistake 3: Choosing a strategy based on company size rather than team topology — Symptom: a 12-person startup copies Google's monorepo approach, spends 3 weeks configuring Bazel, and still hasn't shipped their core feature — Fix: team size is a weak signal. The strong signals are deployment coupling (do services release together?) and shared code percentage (what fraction of your code is shared?). A 500-person company with truly autonomous teams is a better polyrepo candidate than a 20-person team where everyone touches the same shared API layer.

Interview Questions on This Topic

  • QGoogle uses a monorepo with billions of lines of code. Netflix uses hundreds of separate repositories. Walk me through the architectural reasoning behind each choice — what does each company's product and team structure make that the right call?
  • QYour team is migrating from a polyrepo to a monorepo. The biggest concern raised is 'a bad PR in service A could block service B's deployment.' How would you design your CI/CD pipeline to mitigate this risk while still capturing the benefits of the monorepo?
  • QYou have a shared authentication library used by 15 services across your polyrepo. A critical security fix needs to be applied to every service within 24 hours. Walk me through the operational steps required — and then explain how a monorepo would have changed that process.

Frequently Asked Questions

Can you use microservices with a monorepo?

Absolutely — and this is one of the most common misconceptions. Monorepo describes where your source code lives; microservices describes how your application is deployed and run. Google runs millions of microservices from a single monorepo. The code lives together, the services deploy and scale independently. They're orthogonal choices.

What is the biggest disadvantage of a monorepo?

CI/CD complexity and the tooling investment required to keep it fast. As the repo grows, naive pipelines that run all tests on every commit become unusable. You need a build orchestrator that understands your dependency graph (Nx, Turborepo, or Bazel) to run only affected projects. That's real infrastructure work that many teams underestimate when they start.

Is a monorepo the same as a monolith?

No — these are completely different concepts that often get confused. A monolith is a deployment architecture where all functionality ships as a single deployable unit. A monorepo is a source control strategy where multiple distinct projects share one repository. You can have a monorepo full of independently deployed microservices (Google), or a polyrepo where each separate repository contains a slice of one big monolithic application. The two dimensions are independent.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousContinuous Improvement in SoftwareNext →Just-In-Time Compilation
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged