Senior 6 min · March 06, 2026

Monorepo vs Polyrepo — How `affected` Prevents CI Meltdown

Every pull request triggered tests for all 15 projects — 45+ minute CI.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Monorepo: single repo for all projects; polyrepo: separate repos per project
  • Atomic cross-service changes possible only with monorepo and smart CI (Nx, Bazel)
  • Polyrepo gives team autonomy but creates version fragmentation over time
  • Monorepo without build orchestrator becomes unusable beyond 5 projects
  • Decision depends on shared‑code frequency and deployment coupling, not company size
Plain-English First

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.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# ─────────────────────────────────────────────
# 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.
Production Insight
Monorepos scale their coordination cost up front (tooling). Polyrepos defer it — the cost appears later as version fragmentation drift.
If you have a shared library used by 10 services, a polyrepo team will spend ~2 hours every quarter coordinating version bumps across repos.
Rule: track how many hours per quarter your team spends on non‑feature cross‑repo changes — that's your hidden coordination tax.
Key Takeaway
The repo structure determines where coordination overhead lives — in tooling (monorepo) or process (polyrepo).
Neither eliminates the overhead; it only shifts it.
Pick the one whose overhead your team can sustainably manage.

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.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# ─────────────────────────────────────────────────────────────
# 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.
Production Insight
A monorepo that runs all tests for every commit will stall at about 5 projects.
Polyrepo dependency fragmentation starts hurting after 6 months — check npm outdated across all repos to see the drift.
Rule: for monorepo, install a build orchestrator before commit #100. For polyrepo, automate dependency updates before month #3.
Key Takeaway
Don't choose between monorepo and polyrepo based on a single dimension.
Weight: shared code frequency, deployment coupling, and team autonomy.
The right choice today will change as your product evolves — revisit it every 12 months.

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.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// ─────────────────────────────────────────────────────────────
// 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.
Production Insight
In a polyrepo, after 6 months, you'll likely see 3+ different major versions of your shared library in production.
A monorepo eliminates version drift entirely — all consumers see the same code instantly.
But that instant propagation also carries risk: a bug in a shared utility crashes everything at once. Test shared code exhaustively.
Key Takeaway
Sharing code is the axis where the trade-off is sharpest.
If you change shared code weekly, monorepo saves massive coordination cost.
If you change it biannually, polyrepo's version pinning gives you controlled migrations.

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.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
 * 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.
Production Insight
The decision framework is a thought tool, not a calculator — the scores guide discussion but don't replace judgment.
A high monorepo score doesn't mean you must switch overnight; start with one domain as a trial.
Rule: run the scoring exercise for each major product area separately — a monorepo for frontend apps and a polyrepo for backend services is a valid hybrid.
Key Takeaway
The right strategy depends on team topology, not company size.
Use the three‑factor model: shared code %, deployment coupling, and team autonomy.
Hybrid approaches (one monorepo per domain) are often better than either extreme.

Making the Transition: Migrating from One Strategy to the Other

Switching from polyrepo to monorepo? Or the reverse? Both migrations are painful but for different reasons.

Polyrepo → Monorepo: You need to merge Git histories while preserving commit authorship and branches. Use tools like git filter-repo or git-merge-repo. But the real work isn't technical — it's cultural. Teams lose the autonomy to publish on their own cadence. You'll need to agree on a shared CI pipeline, a dependency graph tool, and a release strategy. Expect resistance.

Monorepo → Polyrepo: This is rarer but happens when a monorepo becomes too slow despite tooling, or when a startup gets acquired and the new parent uses a different strategy. You extract each service into its own repo, preserve Git history, and set up independent CI. The technical work is straightforward; the hard part is reestablishing shared library versioning processes.

Whichever direction, do it incrementally. Move one team or one service at a time. Keep the old structure working while the new one stabilises. A big bang migration of 50 repos in a weekend is a recipe for a month of debugging.

migrate_polyrepo_to_monorepo.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# ─────────────────────────────────────────────────────────────
# Migrate one polyrepo into a monorepo component
# Uses git-filter-repo to preserve full history
# ─────────────────────────────────────────────────────────────

# Step 1: In a temporary directory, clone the repo you want to move
cd /tmp
gh repo clone acme/auth-service
cd auth-service

# Step 2: Strip all tags (they won't be meaningful in the monorepo)
git tag -l | xargs git tag -d 2>/dev/null

# Step 3: Rewrite history to pretend all files lived under services/auth-service/
git filter-repo --to-subdirectory-filter services/auth-service --force

# Step 4: Move the rewritten repo into the monorepo clone
cd /path/to/monorepo
# Add the auth-service remote and merge
# Ensure the monorepo is clean

git remote add auth-service /tmp/auth-service
git fetch auth-service --no-tags

# Step 5: Merge all history into main branch
git merge auth-service/main --allow-unrelated-histories --no-edit

# Step 6: Clean up
git remote remove auth-service
rm -rf /tmp/auth-service

echo "Migration complete. Verify with: git log --oneline -- graph services/auth-service/"
Output
Migration complete. Verify with: git log --oneline -- graph services/auth-service/
Migration Pitfall:
Do NOT attempt a history rewrite on a repo that has open pull requests or active branches — you'll orphan them. Plan the migration during a low-activity window and communicate the freeze clearly.
Production Insight
A bad migration can lose commit history, break CI integrations, and demotivate teams.
Plan for a 2‑week buffer for each service you migrate — not because the git operations take that long, but because CI, code review workflows, and onboarding docs must adapt.
Rule: always preserve authorship with git filter-repo; never git rebase --root which loses the initial commit metadata.
Key Takeaway
Migration is 20% technical, 80% people and process.
Move incrementally, one service at a time.
Keep the old repo working in parallel until the new setup is proven.
● Production incidentPOST-MORTEMseverity: high

The Day Our Monorepo CI Ground to a Halt

Symptom
Every pull request triggered full test suite for all 15 projects. CI pipelines took 45+ minutes. Developers began bypassing tests or skipping pre-merge check.
Assumption
We assumed a monorepo just needed standard CI — run tests across all projects. We didn't account for the dependency graph size.
Root cause
No build orchestrator. Every commit ran every test, even when only a single config file changed. The affected concept was missing entirely.
Fix
Introduced Nx with nx affected commands. Configured GitHub Actions to only run tests for projects that actually changed plus their dependents. CI dropped to under 8 minutes.
Key lesson
  • A monorepo without a build orchestrator is a ticking time bomb — the bigger it grows, the slower it gets.
  • Invest in tooling early. The cost of adding Nx or Turborepo after the repo has grown is higher than setting it up from day one.
  • Never assume your CI pipeline will scale horizontally; monorepos need smart, not brute-force, pipeline design.
Production debug guideSymptom → Action guide for the most common monorepo CI debugging scenarios3 entries
Symptom · 01
CI runs all tests on every commit, pipeline takes >20 minutes
Fix
Verify build orchestrator is installed (Nx/Turborepo/Bazel). Check that affected commands are actually used in CI YAML. Run nx affected:test --base=origin/main locally to confirm only changed projects are tested.
Symptom · 02
A test fails but the change only touched a completely unrelated project
Fix
Likely a flaky test or an implicit dependency not captured in the project graph. Run nx graph to see all edges. Add explicit dependsOn in nx.json or project.json if a project uses code from another without declaring it.
Symptom · 03
Build cache not working, same projects rebuild every time
Fix
Check cache location (e.g., Nx cloud, local .nx/cache). Ensure inputs in nx.json include only relevant files. Exclude generated files or node_modules to avoid cache misses.
★ Quick Cheat Sheet: Monorepo CI SlowdownIf your monorepo CI takes >15 minutes per PR, run these commands to diagnose and fix.
Full test suite runs even for a README change
Immediate action
Check if your CI runs `nx affected:test` or plain `nx test:all`
Commands
npx nx print-affected --target=test --base=origin/main
npx nx graph --affected
Fix now
Replace nx test:all with nx affected:test --base=origin/main --parallel=3 in your CI YAML
Monorepo vs Polyrepo Feature Comparison
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

1
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.
2
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.
3
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.
4
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.
5
Migrations are 20% technical, 80% people. Move incrementally, preserve history, and keep both structures alive during transition.

Common mistakes to avoid

3 patterns
×

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

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

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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Google uses a monorepo with billions of lines of code. Netflix uses hund...
Q02SENIOR
Your team is migrating from a polyrepo to a monorepo. The biggest concer...
Q03SENIOR
You have a shared authentication library used by 15 services across your...
Q01 of 03SENIOR

Google 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?

ANSWER
Google has a single product family (search, ads, YouTube, Gmail) where changes frequently need to be atomic across services. Engineers can move between teams easily. Netflix has 200+ microservices, each with independent deploy cadences and team ownership. A monorepo at Netflix would cause too much CI noise and coordination overhead. The difference is deployment coupling: Google has coordinated releases, Netflix has independent releases. Also, Google invests heavily in internal tooling (Bazel, a custom CI system) that makes monorepo scale; Netflix relies on AWS and simpler per-repo pipelines.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can you use microservices with a monorepo?
02
What is the biggest disadvantage of a monorepo?
03
Is a monorepo the same as a monolith?
04
Can I start with a polyrepo and migrate to a monorepo later?
05
Which companies use a hybrid approach?
🔥

That's Software Engineering. Mark it forged?

6 min read · try the examples if you haven't

Previous
Continuous Improvement in Software
13 / 16 · Software Engineering
Next
Conway's Law