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
✦ Definition~90s read
What is Monorepo vs Polyrepo?
A monorepo stores multiple projects in a single repository, while a polyrepo splits each project into its own repository. The choice isn't about ideology—it's about managing dependency graphs and CI/CD blast radius. Monorepos simplify cross-project refactoring and atomic changes (Google, Meta, and Microsoft run monorepos at scale), but they create a single point of failure: a broken commit can block every team.
★
Imagine your family keeps all their documents — tax returns, school reports, recipes, car manuals — in one big shared filing cabinet in the hallway.
Polyrepos isolate failures and scale team autonomy (think open-source ecosystems like npm or PyPI), but they force you into versioned package releases, dependency hell, and duplicated CI pipelines. The real pain point emerges when your CI pipeline must rebuild and test everything on every change—that's where affected tools (Nx, Turborepo, Bazel) become essential.
Without them, a monorepo's CI costs explode linearly with project count, and a polyrepo's cross-repo integration tests become a manual nightmare. The affected command computes exactly which projects changed and only runs their tests, slashing CI time from hours to minutes.
This isn't a silver bullet—it's a pragmatic trade-off: monorepo with affected gives you atomicity without the meltdown, while polyrepo with careful package management gives you isolation without the integration rot. Choose based on your team's tolerance for coordination overhead versus CI complexity.
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
# ─────────────────────────────────────────────
# MONOREPOSTRUCTURE — everything under one roof
# ─────────────────────────────────────────────
# acme-platform/ <── single Git repository
# apps/
# customer-web/ <── React frontend
# admin-dashboard/ <── Internal tools UI
# mobile/ <── ReactNative 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 ""
# ─────────────────────────────────────────────
# POLYREPOSTRUCTURE — 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.
thecodeforge.io
Monorepo vs Polyrepo: CI/CD Strategy
Monorepo Vs Polyrepo
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
# ─────────────────────────────────────────────────────────────
# GitHubActions + Nx: SmartCI that only runs affected projects
# This is the tooling investment a healthy monorepo requires.
# Withoutthis, every commit runs ALL tests — unusable at scale.
# ─────────────────────────────────────────────────────────────
name: AffectedProjectsCI
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 forNx 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
# 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// ─────────────────────────────────────────────────────────────exportinterfaceCurrencyFormatOptions {
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.
* Centralisingthis prevents subtle regional formatting bugs
* (e.g. de-DE uses comma as decimal separator, not period).
*/
exportfunctionformatCurrency(
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;
returnnewIntl.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/srcconst 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.
*
* Runthiswith: 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,
};
functionrecommendRepoStrategy(profile) {
let monorepoScore = 0;
let polyrepoScore = 0;
const reasoning = [];
// Cross-functional engineers benefit massively from monorepo atomic commitsif (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 PRsif (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 itif (!profile.hasDedicatedPlatformTeam && profile.numberOfServices > 8) {
polyrepoScore += 2;
reasoning.push('⚠ No platform team + many services → monorepo CI tooling becomes a maintenance burden');
} elseif (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 manageableif (profile.totalEngineers < 15) {
polyrepoScore += 1;
reasoning.push('✓ Small team → polyrepo simplicity outweighs monorepo tooling setup cost');
} elseif (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(` MonorepoScore : ${result.monorepoScore}`);
console.log(` PolyrepoScore : ${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
# ─────────────────────────────────────────────────────────────
# Step1: In a temporary directory, clone the repo you want to move
cd /tmp
gh repo clone acme/auth-service
cd auth-service
# Step2: Strip all tags (they won't be meaningful in the monorepo)
git tag -l | xargs git tag -d 2>/dev/null
# Step3: Rewrite history to pretend all files lived under services/auth-service/
git filter-repo --to-subdirectory-filter services/auth-service --force
# Step4: 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
# Step5: Merge all history into main branch
git merge auth-service/main --allow-unrelated-histories --no-edit
# Step6: Clean up
git remote remove auth-service
rm -rf /tmp/auth-service
echo "Migration complete. Verify with: git log --oneline -- graph services/auth-service/"
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.
Why Your CI/CD Pipeline Will Hate You (And How to Fix It)
Your CI/CD pipeline doesn't care about your architectural purity. It cares about time, dependencies, and cache misses. Monorepo advocates love to cite "atomic commits" and "single source of truth" while ignoring that every push triggers a rebuild of every service that vaguely touched a shared library. I've seen monorepo CI pipelines take 45 minutes for a one-line config change because the dependency graph looked like a plate of spaghetti. Polyrepo isn't innocent either. Each repo needs its own pipeline, its own artifact registry, and its own release cadence. You'll spend more time writing YAML than actual code. The fix? Start with tooling. Nx, Bazel, or Turborepo for monorepos can shard your builds to only affected projects. For polyrepo, invest in a shared CI template library and enforce semantic versioning with automated release notes. Without this, you're just trading one hell for another.
Never assume your CI tool handles dependency graphs for you. If you don't explicitly configure build scoping, every change in a monorepo triggers the entire suite. That's how you turn a 2-minute deploy into a lunch break.
Key Takeaway
Your CI/CD strategy must be designed before your first merge. Monorepo needs build sharding; polyrepo needs cross-repo orchestration.
The Ownership Trap: Why Teams Drift Apart (or Merge in Chaos)
Polyrepo was supposed to give teams autonomy. Instead, it often gives them permission to reinvent the wheel three times. I've watched three teams implement four different authentication libraries because they couldn't agree on a shared package. The result? A security audit nightmare. Monorepo sounds like the fix — one codebase, one standard. But it introduces a different hell: the "benevolent dictator" problem. One team's code change breaks another's build, and suddenly you're in a meeting about git blame. The ownership pattern that actually works is "code ownership with federated governance." In a monorepo, use CODEOWNERS files and require approvals from the owning team for any changes to their directory. In polyrepo, enforce a shared toolchain with automated linting and API contracts. The goal isn't total isolation or total chaos. It's controlled cooperation. Each team owns their domain, but the plumbing between them must be standardised. Otherwise, you're not building a system. You're building a museum of architectural experiments.
OwnershipPolicy.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — cs-fundamentals tutorial
# Simulating a CODEOWNERS check for a monorepoimport os
team_map = {
"services/payment": "@team-payment",
"services/shipping": "@team-shipping",
"libs/common": "@team-platform",
}
changed_file = "services/payment/src/handler.py"
owners = Nonefor path, team in team_map.items():
if changed_file.startswith(path):
owners = team
breakif owners:
print(f"Requiring approval from {owners}")
else:
print("No ownership defined. Anyone can break this.")
print("Production Incident: 85% chance of unapproved change.")
Output
Requiring approval from @team-payment
Senior Shortcut:
Use a pre-commit hook that runs a script checking CODEOWNERS before allowing a push. If the committer isn't on the list, force a review request. Automate the accountability, don't rely on culture.
Key Takeaway
Code ownership without enforcement is just wishful thinking. Use tooling to gate changes to owned directories, regardless of your repository strategy.
Simplified Dependency Management: Stop the Versioning Madness
Dependency management is the hidden tax on productivity. In a polyrepo, every shared library becomes a versioning negotiation. You bump a version, tag a release, update consumer repos, handle breaking changes across ten different PRs. It's a slow-motion disaster. Monorepo eliminates that entire class of problems. One version of everything, compiled together, tested together. The dependency graph is static. No diamond dependency conflicts, no "works on my machine" because prod is running an older package. You change a core utility, every consumer breaks instantly—in your CI, not in production. This is not about tooling. This is about removing the cognitive overhead of versioning from your development process. If your team spends more than two hours a month wrestling with package versions, you have already lost. Monorepo gives you a single source of truth. Use it.
dependency_check.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — cs-fundamentals tutorial
import json
deffind_dependency_conflicts(repos):
"""Detect version conflicts across polyrepo packages."""
versions = {}
for repo, deps in repos.items():
for pkg, ver in deps.items():
if pkg in versions and versions[pkg] != ver:
print(f"CONFLICT: {pkg} wants {ver} in {repo}, but {versions[pkg]} elsewhere")
versions[pkg] = ver
return versions
repos = {
"service-a": {"auth-lib": "1.2.0", "http-client": "3.0.1"},
"service-b": {"auth-lib": "1.3.0", "http-client": "2.9.8"},
}
find_dependency_conflicts(repos)
Output
CONFLICT: auth-lib wants 1.3.0 in service-b, but 1.2.0 elsewhere
CONFLICT: http-client wants 2.9.8 in service-b, but 3.0.1 elsewhere
Production Trap:
If you polyrepo and see 'works with ^1.2.0' in your package.json, you are running different code in production. The monorepo never lies.
Key Takeaway
One repo, one version—stop the versioning tax.
Faster Iteration for Certain Workflows: Atomic Changes Win
Cross-service refactors in a polyrepo are a coordination nightmare. You open four PRs, wait for four reviews, pray the CI passes in sequence. A monorepo lets you commit an atomic change that touches everything. Database schema change, updated ORM, new API endpoint, updated frontend—one commit, one CI run, one deploy. This is faster for three specific scenarios: foundational layer changes (auth, logging, config), API contract updates, and bug fixes that propagate through the stack. If your team does any of these more than once a month, polyrepo is actively slowing you down. The golden rule: if a change requires coordinated updates in multiple repos, you should have used a monorepo. That is not hyperbole. That is the difference between shipping in an hour and shipping in three days of coordination overhead. Atomic commits are a superpower. Do not leave them on the table.
atomic_change.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — cs-fundamentals tutorial
defupdate_auth_flow(schema, backend, frontend):
"""Simulate an atomic cross-service change in a monorepo."""
changes = [
("database/schema.sql", schema),
("auth-service/main.py", backend),
("web-client/auth.js", frontend),
]
for path, content in changes:
print(f"WRITE: {path}")
print(f"CHANGE: {content[:40]}...")
print("\nCOMMIT: 1 change, 1 CI run, 1 deploy")
update_auth_flow(
"ALTER TABLE users ADD role VARCHAR(50)",
"def get_user_role(user_id): ...",
"if user.role === 'admin': show_panel()"
)
Output
WRITE: database/schema.sql
CHANGE: ALTER TABLE users ADD role VARCHAR(50)...
WRITE: auth-service/main.py
CHANGE: def get_user_role(user_id): ...
WRITE: web-client/auth.js
CHANGE: if user.role === 'admin': show_panel()...
COMMIT: 1 change, 1 CI run, 1 deploy
Senior Shortcut:
When you see a ticket that says 'update API contract', ask yourself: is this a monorepo atomic change or a polyrepo multi-PR death march? Make the right call.
Scalability and Performance Issues: The Monorepo Breaks at Scale—So Do You
Polishing the monorepo dream? Let me tell you where it cracks. At some size—usually around 5,000 developers or 10 million lines—git clone takes 20 minutes. CI has to recompile unchanged code because the build graph is a tangled mess. Your IDE freezes because it's indexing a hundred unrelated services. This is real. Google, Meta, and Microsoft built massive internal tooling to survive this. You probably don't own Bazel. The fix? You don't need a monorepo for everything. The rule is brutal: if your CI pipeline runs for more than 15 minutes per change that only touches one service, your monorepo is too big. Cut it. Use sub-repos, path filters, and build caching. Or split into logically grouped monorepos—sometimes called 'monolith-per-domain.' The biggest lie in the monorepo debate is that it scales infinitely. It does not. But neither does a polyrepo with 300 repositories and no shared version. Pick the scaling strategy that matches your team size, not your ambition.
scaling_check.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — cs-fundamentals tutorial
defmonorepo_health_check(team_size, lines_of_code, ci_minutes):
"""Quick threshold check for monorepo scaling issues."""if team_size > 500and lines_of_code > 5_000_000:
print("WARNING: You need Bazel or similar build system.")
if ci_minutes > 15:
print(f"CI takes {ci_minutes} min — slowdown imminent.")
print("ACTION: Add path filters or split into smaller monorepos.")
if team_size < 50:
print("Monorepo is fine. Stop overthinking.")
monorepo_health_check(team_size=1200, lines_of_code=12_000_000, ci_minutes=22)
Output
WARNING: You need Bazel or similar build system.
CI takes 22 min — slowdown imminent.
ACTION: Add path filters or split into smaller monorepos.
Production Reality:
Monorepo at Google scale requires $millions in tooling. If your org is under 200 engineers, you do not have that problem—you have a different one.
Key Takeaway
Monorepo solves team-coordination problems until it creates tooling problems. Know your pain threshold.
1. Introduction
The debate between monorepo and polyrepo is not about which is objectively better—it's about which trade-offs your organization can live with. Every codebase strategy either optimizes for integration or isolation. Monorepos centralize everything, promising atomic changes and unified tooling. Polyrepos split code into separate repositories, giving teams autonomy to move at their own pace. Neither is wrong, but both fail spectacularly when chosen for the wrong reasons. This article strips away the hype and focuses on two critical dimensions often overlooked: flexibility and agility versus easy maintainability. You'll learn why a startup with five developers should never adopt a monorepo designed for a thousand, and why a regulated financial firm shouldn't chase polyrepo microservices. The goal is to match your strategy to your actual constraints, not to a buzzword.
repo_strategy.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — cs-fundamentals tutorial
import os
REPO_TYPE = os.getenv('REPO_STRATEGY', 'unknown')
TEAM_SIZE = int(os.getenv('TEAM_SIZE', 5))
if REPO_TYPE == 'monorepo'and TEAM_SIZE < 15:
print('Warning: Monorepo overhead may slow small teams.')
elif REPO_TYPE == 'polyrepo'and TEAM_SIZE > 50:
print('Warning: Polyrepo coordination cost grows fast.')
else:
print('Strategy fits team size. Proceed.')
Output
Strategy fits team size. Proceed.
Production Trap:
Don't blindly copy Google's monorepo. They have custom tooling you don't. Start small, measure friction, then scale.
Key Takeaway
Match repository strategy to your current team size and velocity needs, not to industry giants.
3.1. Flexibility and Agility
Flexibility in a polyrepo means each team can choose its own stack, release cycle, and deployment pipeline. If your frontend team wants to move from React to Svelte, they don't need permission from the backend team. This autonomy accelerates experimentation and local decision-making. However, this same flexibility becomes a liability when cross-team changes require coordinated releases across five repositories. Monorepos offer agility in a different way: atomic changes. A single commit can refactor an API, update its consumers, and deploy together. This is a superpower for feature development that touches multiple services. But monorepos impose a rigid tooling and workflow standard—everyone uses the same linter, build system, and test framework. Choose flexibility when your teams operate independently; choose agility when your product requires tightly coupled changes across boundaries.
Polyrepo flexibility is good for decoupled teams; monorepo agility is good for coupled changes. Know which you need.
Key Takeaway
Polyrepos maximize team flexibility; monorepos maximize change agility. Pick based on coupling frequency.
● 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`
Replace nx test:all with nx affected:test --base=origin/main --parallel=3 in your CI YAML
Monorepo vs Polyrepo Feature Comparison
Feature / Aspect
Monorepo
Polyrepo
Sharing code across services
Direct import — no versioning needed
Must publish to a package registry and version-pin
Atomic cross-service changes
Single PR touches all affected code
Requires coordinated PRs across multiple repos
CI/CD complexity
High — needs affected-only build orchestration (Nx, Bazel)
Low per-repo — each pipeline is simple and isolated
Tooling investment required
Significant upfront (Nx, Turborepo, or Bazel setup)
Minimal — standard CI template per repo
Team autonomy
Reduced — all teams see and affect each other's PRs
High — each team fully owns their repo and pipeline
Onboarding experience
Excellent — one clone, full context visible
Fragmented — new devs clone 10+ repos to get started
Dependency version drift
Impossible — one version of each dependency site-wide
Common — services pin different versions over time
Blast radius of a bad merge
Higher — can affect many projects at once
Lower — a bad merge only affects one repo
Code discoverability
High — all code searchable in one place
Low — requires knowing which repo to look in
Real-world examples
Google, Meta, Twitter, Airbnb, Vercel
Netflix, Amazon, most microservices-first orgs
Best suited for
Co-deployed services with frequent shared code changes
Fully 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.
Q02 of 03SENIOR
Your 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?
ANSWER
Implement a build orchestrator (Nx, Turborepo, Bazel) that computes affected projects. Only run tests/build for changed projects and their dependents. Use branch protection rules: merging to main requires passing CI for affected projects only. Service B's CI should not be triggered by changes that don't affect it. Also, enforce that each service has its own deployment workflow (e.g., separate GitHub Actions workflows triggered only when specific folders change). This way, a failed PR in service A never blocks service B's releases. The atomic commit benefit remains for shared code changes.
Q03 of 03SENIOR
You 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.
ANSWER
Polyrepo steps: (1) Patch the auth library repo, bump version, publish. (2) Open 15 PRs across service repos updating the dependency. (3) Request urgent reviews from each team. (4) Merge and deploy each service. This can easily take days. With a monorepo: (1) Patch the shared auth library in the monorepo. (2) Update all import references in a single PR. (3) CI runs tests for all 15 services. (4) One review, one merge, one deployment of the monorepo. The fix is live in under an hour. The trade-off is the higher blast radius of the monorepo change—but for security fixes, speed usually outweighs that risk.
01
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?
SENIOR
02
Your 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?
SENIOR
03
You 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.
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
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.
Was this helpful?
02
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.
Was this helpful?
03
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.
Was this helpful?
04
Can I start with a polyrepo and migrate to a monorepo later?
Yes — this is a common path. Many startups begin with polyrepos for speed, then merge into a monorepo when shared code pain becomes too high. The key is to do the migration incrementally: move one service at a time using git filter-repo to preserve history. Don't attempt a big bang merge. Plan for cultural adaptation alongside the technical work.
Was this helpful?
05
Which companies use a hybrid approach?
Spotify is a well-known example: each squad (cross-functional team) has its own monorepo for their domain (e.g., music playback, search, social), but there are polyrepo boundaries between squads. This gives them the atomic commit benefits within a squad and the autonomy boundaries between squads. Other companies like Microsoft use multiple monorepos for different product divisions.