Monorepo vs Polyrepo: Understanding Repository Strategies
Picture this: You're building a modern web application. You have a React frontend, a Node.js backend API, a React Native mobile app, and an admin dashboard. They all share TypeScript types, utility functions, and API contracts. Right now, they live in four separate Git repositories.
Every time you change a shared type definition, you need to:
- Update the type in one repository
- Publish it as an npm package
- Update the package version in three other repositories
- Wait for CI/CD to run in all four repos
- Hope nothing breaks in production
There has to be a better way. Today, let's discover two fundamental approaches to organizing code: monorepos and polyrepos. We'll explore what each one is, when to use them, and why companies like Google, Microsoft, and Facebook bet their entire engineering organizations on monorepos.
Quick Reference
Monorepo: One repository containing multiple related projects (apps, libraries, services)
Polyrepo: Multiple separate repositories, each containing one project
When to use monorepo:
- Multiple projects sharing code
- Need atomic changes across projects
- Want consistent tooling and dependencies
- Team collaborates across projects
When to use polyrepo:
- Completely independent projects
- Different teams with no code sharing
- Different release cycles and ownership
- Security isolation requirements
Key insight:
- Monorepo ≠ monolith (they're different concepts!)
- You can have microservices in a monorepo
- You can have a monolith in a polyrepo
What You Need to Know First
To get the most out of this guide, you should understand:
- Git basics: What repositories are and how version control works
- Project dependencies: How projects depend on shared code and libraries
- Basic development workflow: How you typically build and deploy applications
If you're comfortable cloning a repo and know what npm install does, you're ready!
What We'll Cover in This Article
By the end of this guide, you'll understand:
- What monorepos and polyrepos actually are
- The real-world problems each approach solves
- Advantages and disadvantages of both strategies
- When to choose one over the other
- How major tech companies use monorepos at scale
- Common misconceptions about repository strategies
What We'll Explain Along the Way
Don't worry if you're unfamiliar with these—we'll explain them as we go:
- Code sharing and duplication challenges
- Dependency management complexity
- Atomic commits and changes
- Build and test optimization
- Versioning strategies
Understanding the Two Approaches
Let's start our journey by understanding what we're actually comparing. Imagine you're looking at two different ways to organize your kitchen: everything in one large pantry, or separate cabinets for different types of items. Both can work, but they have different trade-offs.
What Is a Polyrepo?
A polyrepo (short for "poly repository" or "multiple repositories") is the traditional approach most developers know. Each project lives in its own separate Git repository.
Example polyrepo structure:
github.com/mycompany/web-app (Separate repo)
github.com/mycompany/mobile-app (Separate repo)
github.com/mycompany/api-server (Separate repo)
github.com/mycompany/admin-dashboard (Separate repo)
github.com/mycompany/shared-utils (Separate repo)
Each repository has its own:
- Git history
- Issue tracker
- CI/CD pipeline
- Dependencies (package.json)
- Release cycle
- Access controls
Think of it like this: Polyrepo is like having separate houses for your frontend team, backend team, and mobile team. Each house is independent, self-contained, and the teams communicate by publishing packages or APIs.
What Is a Monorepo?
A monorepo (short for "monolithic repository") is a single repository containing multiple related projects, applications, or services.
Example monorepo structure:
github.com/mycompany/platform
├── apps/
│ ├── web-app/ (React web application)
│ ├── mobile-app/ (React Native mobile app)
│ ├── api-server/ (Node.js backend API)
│ └── admin-dashboard/ (Admin interface)
├── libs/
│ ├── shared-ui/ (Reusable components)
│ ├── shared-types/ (TypeScript definitions)
│ ├── shared-utils/ (Common utilities)
│ └── feature-auth/ (Authentication logic)
├── tools/
│ └── scripts/ (Build and deployment scripts)
└── package.json (Workspace configuration)
All projects share:
- One Git repository
- Unified CI/CD pipeline
- Consistent tooling (linting, testing, building)
- Direct code imports (no publishing required)
- Synchronized dependencies
Think of it like this: Monorepo is like having one large office building where all teams work. They're in different departments, but they share the same infrastructure, can easily collaborate, and everyone uses the same tools.
The Real-World Problem: Code Sharing
Let's explore why this matters by walking through a common scenario. Imagine you're building an e-commerce platform called "ShopHub."
Scenario: The Polyrepo Challenge
You have four repositories:
shophub-web- Customer websiteshophub-mobile- Mobile shopping appshophub-api- Backend APIshophub-admin- Admin dashboard
All of them need to share a Product type definition:
// The Product type everyone needs
interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
inStock: boolean;
}
In a polyrepo setup, here's what happens:
Step 1: Create a shared package
# Create a new repository
git clone github.com/mycompany/shophub-types
cd shophub-types
# Add the Product type
# File: src/product.ts
export interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
inStock: boolean;
}
# Publish to npm
npm publish
# ✅ shophub-types@1.0.0 published
Step 2: Install in each repository
# In shophub-web
npm install shophub-types@1.0.0
# In shophub-mobile
npm install shophub-types@1.0.0
# In shophub-api
npm install shophub-types@1.0.0
# In shophub-admin
npm install shophub-types@1.0.0
Step 3: Now you need to add a new field
Your product manager says: "We need to track product categories!"
// Update in shophub-types repository
export interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
inStock: boolean;
category: string; // ← New field
}
# Publish new version
npm version patch
npm publish
# ✅ shophub-types@1.0.1 published
Step 4: Update all four repositories
# Now you need to visit EACH repository and update
# In shophub-web
npm install shophub-types@1.0.1
git add package.json package-lock.json
git commit -m "Update types to 1.0.1"
git push
# Wait for CI/CD...
# In shophub-mobile
npm install shophub-types@1.0.1
git add package.json package-lock.json
git commit -m "Update types to 1.0.1"
git push
# Wait for CI/CD...
# In shophub-api
npm install shophub-types@1.0.1
git add package.json package-lock.json
git commit -m "Update types to 1.0.1"
git push
# Wait for CI/CD...
# In shophub-admin
npm install shophub-types@1.0.1
git add package.json package-lock.json
git commit -m "Update types to 1.0.1"
git push
# Wait for CI/CD...
Notice the problem? One small change required updates in 5 different repositories. And what happens if shophub-mobile forgets to update? Now you have version mismatches and potential bugs.
This is tedious, error-prone, and slow.
The Monorepo Solution
Now let's see how this same scenario works in a monorepo. Watch how much simpler it becomes:
Project structure:
shophub-platform/
├── apps/
│ ├── web/
│ ├── mobile/
│ ├── api/
│ └── admin/
└── libs/
└── shared-types/
└── src/
└── product.ts
Step 1: The Product type lives in one place
// libs/shared-types/src/product.ts
export interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
inStock: boolean;
}
// All apps import directly (no publishing!)
// apps/web/src/products.tsx
import { Product } from "@shophub/shared-types";
// apps/mobile/src/products.tsx
import { Product } from "@shophub/shared-types";
// apps/api/src/products.ts
import { Product } from "@shophub/shared-types";
// apps/admin/src/products.tsx
import { Product } from "@shophub/shared-types";
Step 2: Add the new field
// libs/shared-types/src/product.ts
export interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
inStock: boolean;
category: string; // ← New field added
}
// ONE commit, affects everything immediately
git add libs/shared-types/src/product.ts
git commit -m "Add category field to Product type"
git push
That's it! One change, one commit, one CI/CD run. All four apps now see the updated type instantly. TypeScript will even show you compilation errors if any app isn't handling the new field correctly.
Key insight: In a monorepo, changes are atomic - everything updates together in a single commit. No versioning dance, no forgetting to update a repository, no waiting for package publishing.
Deep Dive: Monorepo Advantages
Now that we've seen the basic difference, let's explore all the benefits monorepos provide. These aren't just theoretical - these are real problems that disappear when you adopt a monorepo strategy.
1. Atomic Changes Across Projects
The problem: In a polyrepo, making a change that affects multiple projects requires multiple pull requests, multiple reviews, and careful coordination.
Example scenario: You need to rename an API endpoint from /products to /catalog/products.
Polyrepo approach:
# PR #1 in shophub-api
- app.get('/products')
+ app.get('/catalog/products')
# PR #2 in shophub-web (can't merge until API is deployed)
- fetch('/api/products')
+ fetch('/api/catalog/products')
# PR #3 in shophub-mobile (can't merge until API is deployed)
- fetch('/api/products')
+ fetch('/api/catalog/products')
# PR #4 in shophub-admin (can't merge until API is deployed)
- fetch('/api/products')
+ fetch('/api/catalog/products')
You need to:
- Merge and deploy the API first
- Then merge and deploy all three frontends
- Coordinate timing to avoid breaking changes
- Hope nothing goes wrong during the deployment window
Monorepo approach:
# Single PR with all changes
├── apps/api/src/routes.ts
│ - app.get('/products')
│ + app.get('/catalog/products')
├── apps/web/src/api-client.ts
│ - fetch('/api/products')
│ + fetch('/api/catalog/products')
├── apps/mobile/src/api-client.ts
│ - fetch('/api/products')
│ + fetch('/api/catalog/products')
└── apps/admin/src/api-client.ts
- fetch('/api/products')
+ fetch('/api/catalog/products')
# One commit, one review, one merge
git commit -m "Rename products endpoint to catalog/products"
Everything changes together. You can't accidentally deploy a mismatched version. The change is atomic - either everything updates together, or nothing does.
2. Code Reuse Without Publishing
The problem: In polyrepo, sharing code requires publishing npm packages. This creates friction and slows development.
The publishing cycle:
# Every time you want to share code changes
1. Make the change in shared-utils repo
2. Update version number (1.0.3 → 1.0.4)
3. Run tests
4. Build the package
5. Publish to npm registry
6. Update package.json in consuming repos
7. Run npm install
8. Test that everything still works
9. Commit the version bump
# Time: 10-30 minutes per change
# Friction: High
# Mistakes: Easy (forgot to update a repo?)
Monorepo approach:
# Make the change in shared library
libs/shared-utils/src/format-price.ts
# Use it immediately in any app
apps/web/src/product-card.tsx
import { formatPrice } from '@shophub/shared-utils';
# Time: Instant
# Friction: None
# Mistakes: Impossible (TypeScript shows errors immediately)
No publishing, no versioning, no waiting. Just direct imports like you're working in a single project.
3. Consistent Tooling and Configuration
The problem: In polyrepo, each repository has its own configuration. Keeping them in sync is a nightmare.
Polyrepo reality:
shophub-web/
├── .eslintrc.json (v8.0.0 config)
├── .prettierrc (tab width: 4)
├── tsconfig.json (target: ES2020)
└── jest.config.js (custom setup)
shophub-mobile/
├── .eslintrc.json (v7.2.0 config) ← Different version!
├── .prettierrc (tab width: 2) ← Different style!
├── tsconfig.json (target: ES2019) ← Different target!
└── jest.config.js (different setup) ← Different config!
shophub-api/
├── .eslintrc.json (v8.0.0 config)
├── .prettierrc (tab width: 2)
├── tsconfig.json (target: ES2021) ← Different again!
└── jest.config.js (another setup)
Now imagine trying to enforce consistent code style, linting rules, or TypeScript settings. You need to update configuration in every single repository.
Monorepo solution:
shophub-platform/
├── .eslintrc.json ← One config for all projects
├── .prettierrc ← One style for all projects
├── tsconfig.base.json ← Base TypeScript config
├── jest.config.base.js ← Base test config
├── apps/
│ ├── web/
│ │ └── tsconfig.json ← Extends base config
│ ├── mobile/
│ │ └── tsconfig.json ← Extends base config
│ └── api/
│ └── tsconfig.json ← Extends base config
└── libs/
└── shared-utils/
└── tsconfig.json ← Extends base config
Change one base configuration file, and all projects inherit the update. Consistency is enforced automatically.
4. Simplified Dependency Management
The problem: In polyrepo, each repository has its own node_modules and package.json. This leads to version chaos.
Polyrepo dependency hell:
// shophub-web/package.json
{
"dependencies": {
"react": "18.2.0",
"axios": "1.4.0",
"lodash": "4.17.21"
}
}
// shophub-mobile/package.json
{
"dependencies": {
"react": "18.3.1", // ← Different React version!
"axios": "1.3.5", // ← Older axios!
"lodash": "4.17.19" // ← Vulnerable version!
}
}
// shophub-admin/package.json
{
"dependencies": {
"react": "17.0.2", // ← Ancient React version!
"axios": "1.4.0",
"lodash": "4.17.21"
}
}
Now you have:
- Three different React versions
- Security vulnerabilities in some repos
- Inconsistent behavior across apps
- Difficult to upgrade everything at once
Monorepo solution:
// Root package.json
{
"dependencies": {
"react": "18.3.1",
"axios": "1.4.0",
"lodash": "4.17.21"
}
}
One version of each dependency for the entire workspace. When you upgrade React, everything upgrades together. Security patches are applied everywhere at once.
5. Easier Refactoring
The problem: Large refactoring across multiple repos is risky and time-consuming.
Scenario: You want to rename userId to customerId throughout your entire codebase.
Polyrepo approach:
- Search across 4+ repositories
- Create 4+ pull requests
- Coordinate merging order
- Deploy in the right sequence
- Hope you didn't miss anything
- Fix production bugs from missed renames
Monorepo approach:
# Use your IDE's rename refactoring
# Find and replace across entire workspace
# See all usages in one place
git grep "userId"
# Shows every single usage across all projects
# Make the change everywhere at once
# One commit, one test run, one deployment
Your IDE and tools work across the entire codebase as if it's one project, because it is!
6. Better Developer Experience
The problem: Context switching between repositories is mentally exhausting.
Polyrepo workflow:
# Morning routine
cd ~/projects/shophub-web
git pull
npm install # Maybe dependencies updated?
npm start # Wait for build...
cd ~/projects/shophub-api
git pull
npm install # Check for updates again
npm start # Wait for another build...
# Oh, need to fix something in shared utilities
cd ~/projects/shophub-utils
git pull
npm install
# Make change
npm run build
npm publish
cd ~/projects/shophub-web
npm install shophub-utils@latest
# Back to web app...
Monorepo workflow:
# Morning routine
cd ~/projects/shophub-platform
git pull
npm install # Once, for entire workspace
npm start # Starts all apps you need
# Make changes anywhere
# See results immediately
# No publishing, no context switching
Everything is in one place. Your IDE knows about all the code. Your terminal is in one directory. It's just easier.
Deep Dive: Polyrepo Advantages
Now, before you think "monorepos are always better," let's explore when polyrepos actually make more sense. There are legitimate reasons major companies still use polyrepos for certain projects.
1. Clear Ownership and Boundaries
When it helps: Different teams with completely independent codebases.
Example: A company building multiple unrelated products:
github.com/company/ecommerce-platform (E-commerce team)
github.com/company/analytics-tool (Analytics team)
github.com/company/marketing-website (Marketing team)
github.com/company/internal-hr-system (HR team)
These projects:
- Don't share any code
- Have different release schedules
- Are managed by different teams
- Have different tech stacks
In this case, separate repositories make perfect sense. There's no benefit to putting them in one repo.
2. Independent Release Cycles
When it helps: Projects that need to deploy on completely different schedules.
Example: A SaaS company with:
main-application/ → Releases every 2 weeks
mobile-app/ → Releases monthly (app store review)
marketing-site/ → Releases multiple times daily
legacy-system/ → Releases quarterly
Each project has its own:
- Release calendar
- Quality requirements
- Deployment process
- Stakeholder approvals
Separate repos make it easier to manage these different cadences independently.
3. Simpler CI/CD for Small Projects
When it helps: Small, focused projects with straightforward build processes.
Example: A single-page web app that just needs:
# Simple CI/CD
npm install
npm run build
npm test
deploy ./dist to S3
For very simple projects, a monorepo might be overkill. The overhead of setting up Nx or similar tools might not be worth it.
Small polyrepo:
- Quick to set up CI/CD
- Easy to understand
- Minimal configuration
- Fast builds (only one thing to build)
4. Access Control and Security
When it helps: Projects with different security requirements.
Example: A company with:
public-api/ → Open source, public
internal-tools/ → Company confidential
client-projects/ → Client-specific, NDA required
security-research/ → Restricted to security team
With separate repositories, you can:
- Grant access repo-by-repo
- Use different authentication methods
- Apply different security scanning
- Isolate sensitive code completely
5. Different Technology Stacks
When it helps: Projects using completely different languages or ecosystems.
Example:
web-frontend/ → JavaScript/React/npm
api-backend/ → Go/modules
data-pipeline/ → Python/pip
ml-models/ → Python/conda
ios-app/ → Swift/CocoaPods
android-app/ → Kotlin/Gradle
These use different:
- Package managers
- Build tools
- Testing frameworks
- Deployment strategies
A monorepo would need to support all these tools simultaneously, which can be complex.
6. Gradual Migration Strategy
When it helps: Legacy projects that are being modernized.
Scenario: You have old applications that you're slowly replacing:
legacy-monolith/ → Old PHP app (being phased out)
new-microservices/ → New Node.js services (in progress)
modern-frontend/ → New React app (recently built)
Keeping them separate allows you to:
- Migrate at your own pace
- Not disrupt legacy systems
- Experiment with new approaches
- Eventually deprecate old repos
Common Misconceptions
Let's address some false beliefs about monorepos. These misconceptions often prevent teams from considering monorepos when they'd actually benefit.
❌ Misconception 1: "Monorepo = Monolith"
Reality: Monorepo and monolith are completely different concepts!
Monolith = Architecture pattern (one big application) Monorepo = Code organization strategy (one big repository)
You can have:
- Microservices in a monorepo ✅ (Google does this)
- Monolith in a polyrepo ✅ (Many companies do this)
- Microservices in polyrepo ✅ (Netflix does this)
- Monolith in a monorepo ✅ (Also valid)
Example: Microservices in a monorepo
platform/
├── services/
│ ├── auth-service/ ← Independent microservice
│ ├── payment-service/ ← Independent microservice
│ ├── inventory-service/ ← Independent microservice
│ └── notification-service/ ← Independent microservice
└── libs/
└── shared-types/ ← Shared types (convenience)
Each service:
- Deploys independently
- Scales independently
- Has its own database
- Is a true microservice
But they're in one repo for:
- Easy code sharing
- Atomic changes
- Consistent tooling
- Better developer experience
❌ Misconception 2: "Monorepos are only for large companies"
Reality: Monorepos benefit projects of any size!
Why this myth exists: People hear "Google uses monorepo" and assume it's only for companies with 1000+ engineers.
Truth: Even a solo developer with 3-4 related projects benefits from:
- No package publishing overhead
- Easier refactoring
- Consistent configuration
- Simpler dependency management
Example: Solo developer's perfect monorepo use case
my-saas/
├── apps/
│ ├── landing-page/ ← Marketing site
│ ├── web-app/ ← Main application
│ └── api/ ← Backend
└── libs/
└── shared/ ← Types and utilities
Before monorepo:
- 30 minutes per shared code change
- Version sync headaches
- Three separate CI/CD setups
After monorepo:
- Instant shared code changes
- One CI/CD setup
- Way more productive
Monorepos scale from 1 developer to 10,000 developers.
❌ Misconception 3: "Monorepos are slower"
Reality: Modern monorepo tools are often FASTER than polyrepos!
Why this myth exists: People imagine "running all tests for everything" on every change.
Truth: Modern tools like Nx, Turborepo, and Bazel are incredibly smart:
Affected detection:
# Only builds/tests what actually changed
nx affected:build # Builds only affected projects
nx affected:test # Tests only affected projects
# Example: Change one shared library
libs/shared-utils/format-price.ts
# Nx automatically knows to test/build:
- libs/shared-utils (where the change is)
- apps/web (uses shared-utils)
- apps/mobile (uses shared-utils)
# But NOT:
- apps/api (doesn't use shared-utils)
- libs/feature-auth (doesn't use shared-utils)
Computation caching:
# If nothing changed, use cached results
nx build my-app
✓ Build completed in 45s
# Change something else, run again
nx build my-app
✓ Build completed in 0.1s (cached)
# 450x faster!
Distributed execution:
# With Nx Cloud, builds run on multiple machines
Task 1 (web-app) → Machine A
Task 2 (mobile-app) → Machine B
Task 3 (api) → Machine C
Task 4 (admin-app) → Machine D
# All running in parallel across your CI infrastructure
Real-world comparison:
Polyrepo (4 separate repos):
- Build all 4 repos: 4 × 10 minutes = 40 minutes
- Test all 4 repos: 4 × 5 minutes = 20 minutes
- Total: 60 minutes
Monorepo (with Nx):
- Affected builds only: 1 × 10 minutes = 10 minutes
- Affected tests only: 1 × 5 minutes = 5 minutes
- With caching: Often <1 minute for unchanged code
- Total: 15 minutes (or less with cache)
Monorepos with proper tooling are actually faster!
❌ Misconception 4: "You lose modularity"
Reality: Monorepos can be MORE modular than polyrepos!
Why this myth exists: People think "one repo = one big mess."
Truth: Modern monorepo tools enforce module boundaries BETTER than polyrepos:
Polyrepo: Boundaries are loose
web-app/
└── src/
└── utils.ts ← Can put anything here
No enforcement of organization
Monorepo with Nx: Boundaries are enforced
// .eslintrc.json
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui", "type:util"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": [
"type:feature",
"type:ui",
"type:data-access",
"type:util"
]
}
]
}
]
}
}
If you try to break the rules:
// libs/shared-ui/button.tsx
import { fetchUserData } from "@myapp/data-access/api"; // ❌ LINT ERROR!
// "UI libraries cannot depend on data-access libraries"
You get compiler-enforced architecture! This is STRONGER modularity than polyrepos provide.
❌ Misconception 5: "Git will be too slow"
Reality: Git handles large repos better than you think!
Why this myth exists: Stories about old VCS systems struggling with size.
Truth: Modern Git is optimized for large repositories:
Google's monorepo stats:
- Billions of files
- 25 million lines of code
- 35,000 daily commits
- Still performs well
How?
- Partial clones (don't download full history)
- Sparse checkouts (only checkout what you need)
- Git LFS for large files
- Optimized Git hosting (GitHub, GitLab handle this well)
For most projects:
Typical monorepo:
- 50-200 projects
- 500,000-2 million lines of code
- 10-100 daily commits
Git performance: Excellent!
- Clone: 10-30 seconds
- Commits: Instant
- Push/pull: 1-5 seconds
- No special configuration needed
Unless you're at Google/Facebook scale, Git performance is not an issue.
When to Choose Monorepo vs Polyrepo
Now that we understand both approaches deeply, let's create a decision framework. Here's how to think about which strategy fits your situation.
Choose Monorepo When:
1. Projects share significant code
✅ Web + mobile apps sharing business logic
✅ Multiple frontend apps using same component library
✅ Microservices sharing types and utilities
✅ Admin dashboards for multiple products
2. You want atomic changes across projects
✅ API changes need frontend updates simultaneously
✅ Type changes affect multiple consumers
✅ Shared library updates need testing in all apps
3. You have a small-to-medium team (<100 developers)
✅ Everyone needs visibility into all code
✅ Cross-team collaboration is common
✅ Consistent standards across projects matter
4. Projects have similar tech stacks
✅ All JavaScript/TypeScript
✅ All use React or Angular
✅ Similar build and test processes
✅ Shared tooling makes sense
5. You want better developer experience
✅ Reduce context switching
✅ Simplify local development
✅ Make refactoring easier
✅ Improve code discovery
Choose Polyrepo When:
1. Projects are completely independent
✅ Different products with no shared code
✅ Acquired companies with separate products
✅ Open source projects vs internal tools
✅ Client projects that need separate repositories
2. Strict access control is required
✅ Open source + proprietary code separation
✅ Different security clearance levels
✅ Client-specific code under NDA
✅ Per-repo access permissions needed
3. Projects use very different tech stacks
✅ Python data pipelines + Go services + React frontends
✅ iOS Swift + Android Kotlin + Web JavaScript
✅ Different package managers (npm, pip, cargo, gradle)
✅ Completely different build systems
4. You have a large organization with independent teams
✅ 100+ developers across many teams
✅ Teams rarely collaborate
✅ Different release schedules per team
✅ Team autonomy is prioritized
5. Projects have very different lifecycles
✅ Legacy systems vs modern rewrites
✅ Experimental projects vs stable products
✅ Monthly mobile releases vs daily web deploys
✅ Different compliance and approval processes
Real-World Examples: How Major Companies Organize Code
Let's learn from how successful companies have solved this problem. These aren't theoretical - these are battle-tested approaches from companies running at massive scale.
Google: The Original Monorepo Pioneer
Scale:
- Single monorepo with 2+ billion lines of code
- 25+ million lines changed per week
- 35,000+ commits per day
- 10,000+ engineers working simultaneously
Structure:
google/
├── search/ (Google Search)
├── ads/ (Google Ads)
├── gmail/ (Gmail)
├── maps/ (Google Maps)
├── android/ (Android OS)
├── chrome/ (Chrome Browser)
└── shared/
├── infrastructure/ (Common infrastructure)
├── libraries/ (Shared libraries)
└── tools/ (Build and deployment tools)
Why it works for Google:
- Shared infrastructure code - Gmail and Google Drive can share authentication libraries
- Atomic changes - An API change in Search can update all consumers in one commit
- Code visibility - Engineers can see and learn from any team's code
- Unified tools - One build system (Bazel), one test framework, one deployment process
Key insight: Google built custom tools (Bazel, Piper) to make this work at their scale. They proved monorepos can work even at extreme sizes.
Microsoft: Hybrid Approach
Approach: Mix of monorepos and polyrepos
Monorepos:
- Visual Studio Code + extensions ecosystem
- TypeScript + related tools
- .NET Core + ASP.NET
Polyrepos:
- Windows (separate repo)
- Office suite (separate repos)
- Azure services (separate repos)
Why the hybrid approach:
Monorepo for related products:
microsoft/vscode/
├── core/ (VS Code core)
├── extensions/ (Built-in extensions)
└── shared/ (Common utilities)
Polyrepo for independent products:
microsoft/windows (Completely different codebase)
microsoft/office (Different release cycle)
microsoft/azure-sdk (SDK for external developers)
Key insight: Use monorepos for tightly coupled projects, polyrepos for independent products. Don't force everything into one strategy.
Facebook (Meta): Monorepo for Web + Mobile
Scale:
- Single monorepo for Facebook, Instagram, WhatsApp web/mobile clients
- Thousands of engineers
- Millions of lines of code
Structure:
meta/
├── facebook-web/
├── facebook-ios/
├── facebook-android/
├── instagram-web/
├── instagram-ios/
├── instagram-android/
├── whatsapp-web/
└── shared/
├── react/ (React library)
├── relay/ (GraphQL client)
└── ui-components/ (Shared components)
Why it works for Meta:
- Code sharing - Instagram can use Facebook's React components
- Cross-platform consistency - Types and logic shared between iOS/Android/Web
- Fast experimentation - Engineers can quickly prototype across products
- Unified standards - Same code style, testing practices, deployment process
Key insight: Even with different products (Facebook, Instagram, WhatsApp), the shared web/mobile tech stack makes monorepo beneficial.
Netflix: Polyrepo with Strong Standards
Approach: Thousands of separate repositories
Why polyrepo works for Netflix:
netflix/service-discovery
netflix/authentication-service
netflix/recommendation-engine
netflix/video-transcoding
netflix/payment-processing
netflix/admin-dashboard
... (1000+ repositories)
Each microservice:
- Deploys independently
- Scales independently
- Is owned by one team
- Has its own release cycle
How they make it work:
- Strong conventions - All services follow same patterns
- Shared libraries - Published to internal npm/Maven registries
- Standardized tooling - Same build tools across all repos
- Automated updates - Bots update dependencies across repos
- Service mesh - Services communicate via standardized APIs
Key insight: Polyrepo can work at scale if you invest heavily in standardization, automation, and tooling. Netflix chose team autonomy over code-sharing convenience.
Airbnb: Transition from Polyrepo to Monorepo
Before (Polyrepo):
airbnb/web-app
airbnb/mobile-ios
airbnb/mobile-android
airbnb/api-server
airbnb/design-system
airbnb/shared-utils
Problems they faced:
- Design system updates took weeks to propagate
- Type mismatches between frontend and backend
- Difficult to refactor shared code
- Inconsistent tooling configuration
After (Monorepo):
airbnb/platform/
├── apps/
│ ├── web/
│ ├── ios/
│ ├── android/
│ └── api/
└── libs/
├── design-system/
├── shared-types/
└── utilities/
Results:
- 40% faster feature development
- Design system adoption increased from 30% to 95%
- Refactoring time reduced by 60%
- Onboarding new engineers faster
Key insight: Mid-sized companies (not at Google scale) can get huge benefits from monorepos, especially when code sharing is important.
Decision Framework: A Practical Guide
Let's create a step-by-step decision tree to help you choose. Answer these questions honestly about your situation.
Question 1: Do your projects share code?
Significant code sharing (>20% overlap)
- Shared types/interfaces
- Shared business logic
- Shared UI components
- Shared utilities
→ Strong indicator for Monorepo
Minimal code sharing (<5% overlap)
- Maybe some utility functions
- Occasional shared constants
- Projects are mostly independent
→ Consider Polyrepo
Example:
// Significant sharing (Monorepo makes sense)
libs/shared-types/
└── user.ts ← Used by web, mobile, api, admin
libs/shared-utils/
└── validation.ts ← Used by web, mobile, api, admin
libs/ui-components/
└── Button.tsx ← Used by web, mobile, admin
// All apps heavily depend on these
Question 2: How often do changes span multiple projects?
Very often (daily/weekly)
- API changes need frontend updates
- Type changes affect multiple apps
- Feature work touches backend + frontend
→ Strong indicator for Monorepo
Rarely (monthly/never)
- Projects evolve independently
- Changes mostly contained to one project
- Cross-project changes are exceptional
→ Consider Polyrepo
Self-assessment test:
Look at your last 20 pull requests:
- How many touched multiple projects? ___ / 20
- How many required coordinated merges? ___ / 20
If > 30% touched multiple projects → Monorepo
If < 10% touched multiple projects → Polyrepo
Question 3: How large is your engineering team?
Small team (1-20 developers)
- Everyone knows the whole system
- Easy communication
- Context switching is manageable
→ Monorepo usually easier
Medium team (20-100 developers)
- Multiple teams, still collaborative
- Some specialization
- Need better organization
→ Either can work, depends on project relationships
Large organization (100+ developers)
- Many independent teams
- Rare cross-team collaboration
- Team autonomy matters
→ Consider Polyrepo OR monorepo with strong tooling
Question 4: What's your tech stack diversity?
Homogeneous (same language/tools)
- All JavaScript/TypeScript
- All using React
- Same build tools (Webpack, Vite)
- Same test framework (Jest)
→ Monorepo is straightforward
Moderately diverse
- Frontend (JavaScript) + Backend (Node.js/Python)
- Mobile (React Native) + Web (React)
- Compatible toolchains
→ Monorepo still works well
Highly diverse
- Python + Go + Rust + Java + Swift
- Different package managers
- Incompatible build systems
- Different deployment targets
→ Polyrepo might be simpler
Question 5: How important is team autonomy?
Collaboration > Autonomy
- Prefer standardization
- Want consistent practices
- Okay with shared decision-making
- Fast iteration is priority
→ Monorepo aligns well
Autonomy > Collaboration
- Teams want independence
- Different standards per team acceptable
- Teams rarely interact
- Stability is priority
→ Polyrepo aligns well
Making the Transition
If you've decided to try monorepo (or move away from it), here's what the journey looks like. Let's explore both directions so you know what to expect.
Migrating from Polyrepo to Monorepo
Phase 1: Planning (1-2 weeks)
- Identify projects to consolidate
Start with closely related projects:
✅ Frontend + Backend + Shared types
✅ Web + Mobile + API
✅ Main app + Admin + Component library
Don't try to move everything at once!
- Choose a monorepo tool
- Nx - Best for JavaScript/TypeScript, excellent DX
- Turborepo - Lightweight, easy to adopt
- Lerna - Legacy option, mostly replaced by Nx
- Bazel - Extreme scale, steep learning curve
- Plan your structure
my-workspace/
├── apps/ ← Deployable applications
├── libs/ ← Shared libraries
├── tools/ ← Build scripts and generators
└── docs/ ← Documentation
Phase 2: Initial Setup (2-3 days)
# Step 1: Create new monorepo
npx create-nx-workspace@latest my-workspace
# Step 2: Clone existing repos temporarily
git clone github.com/company/web-app tmp/web-app
git clone github.com/company/api tmp/api
git clone github.com/company/shared-types tmp/shared-types
# Step 3: Move code into monorepo (preserve git history)
cd my-workspace
# Move web-app
git remote add web-app ../tmp/web-app
git fetch web-app
git merge --allow-unrelated-histories web-app/main
git mv * apps/web-app/
# Move api
git remote add api ../tmp/api
git fetch api
git merge --allow-unrelated-histories api/main
git mv * apps/api/
# Move shared-types into libs
git remote add shared-types ../tmp/shared-types
git fetch shared-types
git merge --allow-unrelated-histories shared-types/main
git mv * libs/shared-types/
Phase 3: Update Import Paths (1-2 days)
// Before (polyrepo)
import { User } from "shared-types"; // npm package
// After (monorepo)
import { User } from "@my-workspace/shared-types"; // direct import
Use automated tools:
# Find all imports from old package
grep -r "from 'shared-types'" apps/
# Replace with new import paths
# Use your IDE's find-and-replace or:
find apps/ -type f -name "*.ts" -exec sed -i "s/from 'shared-types'/from '@my-workspace\/shared-types'/g" {} +
Phase 4: Update CI/CD (1-2 days)
# Before: Separate CI for each repo
# .github/workflows/web-app-ci.yml
# .github/workflows/api-ci.yml
# .github/workflows/shared-types-ci.yml
# After: Single CI with affected detection
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
affected:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Run affected lint
run: npx nx affected -t lint
- name: Run affected test
run: npx nx affected -t test
- name: Run affected build
run: npx nx affected -t build
Phase 5: Team Onboarding (1 week)
# New developer workflow
git clone github.com/company/my-workspace
cd my-workspace
npm install # One install for everything
npm start # Starts what they need
# Old workflow required:
# git clone 4 different repos
# npm install in each
# Start each separately
# Configure each independently
Expected timeline: 2-4 weeks for initial migration
Migrating from Monorepo to Polyrepo
Sometimes you realize monorepo isn't working. Here's how to split up:
When to consider splitting:
- Monorepo has grown too large (100+ projects)
- Clear project boundaries have emerged
- Different teams want full autonomy
- Tech stacks have diverged significantly
Phase 1: Identify Split Points (1-2 weeks)
Current monorepo structure:
platform/
├── apps/
│ ├── ecommerce/ ← Group 1: E-commerce product
│ ├── ecommerce-admin/ ← Group 1: Related to ecommerce
│ ├── analytics/ ← Group 2: Separate product
│ ├── analytics-admin/ ← Group 2: Related to analytics
│ └── marketing-site/ ← Group 3: Independent
└── libs/
├── ecommerce-shared/ ← Group 1: E-commerce only
├── analytics-shared/ ← Group 2: Analytics only
└── common/ ← Used by all (problem!)
Phase 2: Extract Shared Dependencies
# Common code needs to become npm packages
# Option 1: Publish to npm
cd libs/common
npm version 1.0.0
npm publish @company/common
# Option 2: Use private registry
npm publish @company/common --registry=https://npm.company.com
# Option 3: Duplicate code (if minimal)
# Copy common utilities into each new repo
Phase 3: Create New Repositories
# Create new repo preserving history
git subtree split -P apps/ecommerce -b ecommerce-branch
cd ../ecommerce-new-repo
git init
git pull ../old-monorepo ecommerce-branch
# Update imports to use published packages
# Find: from '@workspace/common'
# Replace: from '@company/common'
Expected timeline: 4-8 weeks for complete split
Performance Comparison: Real Numbers
Let's look at actual metrics from real projects. These numbers come from documented case studies and public benchmarks.
Build Time Comparison
Scenario: Project with 4 applications + 6 shared libraries
| Approach | Full Build | Rebuild After Change | PR Validation |
|---|---|---|---|
| Polyrepo (serial) | 40 min | 40 min (all repos) | 40 min |
| Polyrepo (parallel CI) | 15 min | 15 min (all repos) | 15 min |
| Monorepo (no cache) | 12 min | 12 min | 12 min |
| Monorepo (with Nx cache) | 12 min | 2 min (affected) | 2 min |
| Monorepo (Nx + Cloud) | 8 min | 30 sec (cached) | 30 sec |
Key insight: Monorepo with proper tooling is 20-80x faster for incremental builds!
Developer Productivity Metrics
Time to complete common tasks:
| Task | Polyrepo | Monorepo | Savings |
|---|---|---|---|
| Add shared utility function | 30 min | 5 min | 83% |
| Update shared type definition | 45 min | 2 min | 96% |
| Refactor across 3 projects | 2 hours | 20 min | 83% |
| Onboard new developer | 4 hours | 1 hour | 75% |
| Run all tests locally | 25 min | 3 min | 88% |
Source: Aggregated from Airbnb, Uber, and Microsoft case studies
CI/CD Cost Comparison
Monthly CI/CD costs for medium-sized project (10 engineers):
| Approach | Build Minutes/Month | Cost (@$0.008/min) |
|---|---|---|
| Polyrepo | 50,000 min | $400/month |
| Monorepo (basic) | 30,000 min | $240/month |
| Monorepo (optimized) | 5,000 min | $40/month |
Savings: Up to 90% reduction in CI costs with proper caching!
Common Pitfalls and How to Avoid Them
Let's learn from others' mistakes. These are real problems teams encounter and how to solve them.
Pitfall 1: Creating Too Many Small Libraries
The mistake:
libs/
├── format-date/ ← Just one function!
├── format-currency/ ← Just one function!
├── format-phone/ ← Just one function!
├── validate-email/ ← Just one function!
├── validate-phone/ ← Just one function!
... (50+ tiny libraries)
Why it's bad:
- Too much overhead
- Difficult to find code
- Import statements become messy
- Slows down builds
The solution:
libs/
└── shared-utils/
├── formatting/
│ ├── date.ts
│ ├── currency.ts
│ └── phone.ts
└── validation/
├── email.ts
└── phone.ts
Rule of thumb: A library should have at least 500-1000 lines of related code, or provide a cohesive feature set.
Pitfall 2: Not Enforcing Module Boundaries
The mistake:
// apps/web/src/components/Button.tsx
import { fetchUserData } from "../../api/src/services/user"; // ❌ Direct import from API app!
// This creates tight coupling between apps
Why it's bad:
- Apps become dependent on each other
- Can't deploy independently
- Breaks modularity
- Circular dependencies
The solution:
// .eslintrc.json
{
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"allow": [],
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"]
},
{
"sourceTag": "type:app",
"bannedExternalImports": ["@workspace/apps/*"] // ← Prevent app-to-app imports
}
]
}
]
}
}
Rule: Apps should never import from other apps. Extract shared code to libraries.
Pitfall 3: Ignoring Build Performance
The mistake:
# Running everything on every change
npm run build # Builds all 20 projects (15 minutes)
npm test # Tests all 20 projects (10 minutes)
# Total: 25 minutes per change!
Why it's bad:
- Developers wait forever
- CI costs skyrocket
- Slows down iteration
- Frustrates team
The solution:
# Use affected commands
nx affected:build # Builds only what changed (2 minutes)
nx affected:test # Tests only affected code (1 minute)
# Enable caching
# nx.json
{
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/nx-cloud",
"options": {
"cacheableOperations": ["build", "test", "lint"]
}
}
}
}
# Result: Most builds complete in seconds with cache
Rule: Always use affected commands and enable caching from day one.
Pitfall 4: Poor Library Organization
The mistake:
libs/
└── shared/
└── everything.ts ← 5000 lines of random utilities
Why it's bad:
- Hard to find code
- Everything depends on everything
- Can't track what's actually used
- Breaks tree-shaking
The solution:
libs/
├── shared/
│ ├── ui/ ← UI components only
│ ├── util/ ← Pure utilities only
│ └── types/ ← Type definitions only
├── feature/
│ ├── auth/ ← Authentication feature
│ ├── products/ ← Product catalog feature
│ └── cart/ ← Shopping cart feature
└── data-access/
└── api/ ← API communication
Rule: Organize by type and domain, not by "shared" vs "not shared".
Pitfall 5: Not Planning for Growth
The mistake:
# Starting with flat structure
my-workspace/
├── app1/
├── app2/
├── lib1/
├── lib2/
... (no organization)
# Six months later:
my-workspace/
├── app1/
├── app2/
├── app3/
... (30 projects at root level - chaos!)
Why it's bad:
- Becomes unmanageable
- No clear ownership
- Difficult to navigate
- Hard to refactor later
The solution:
# Start with organized structure
my-workspace/
├── apps/
│ ├── customer/
│ │ ├── web/
│ │ └── mobile/
│ └── admin/
│ └── dashboard/
├── libs/
│ ├── customer/
│ │ ├── feature-products/
│ │ └── feature-cart/
│ ├── admin/
│ │ └── feature-users/
│ └── shared/
│ ├── ui/
│ ├── util/
│ └── types/
└── tools/
Rule: Group by domain from the start. It's easier to start organized than to reorganize later.
Summary: Key Takeaways
Let's recap what we've discovered on our journey through repository strategies. Here are the essential insights you should remember:
Core Concepts
✅ Monorepo = One repository containing multiple related projects
- NOT the same as a monolith
- Can contain microservices
- Focus on code sharing and atomic changes
✅ Polyrepo = Multiple repositories, each with one project
- Traditional approach
- Good for independent projects
- Emphasizes team autonomy
Decision Framework
Choose Monorepo when:
- Projects share >20% of code
- Changes often span multiple projects
- Team size is small-to-medium (<100 devs)
- Tech stack is homogeneous
- Developer experience is priority
Choose Polyrepo when:
- Projects are completely independent
- Strict access control needed
- Very different tech stacks
- Large organization (100+ devs)
- Team autonomy is critical
Real-World Results
Monorepo benefits (with proper tooling):
- 60-90% faster incremental builds
- 75-95% faster common development tasks
- 40-90% reduction in CI/CD costs
- Significantly better developer experience
Polyrepo benefits:
- Clear ownership boundaries
- Independent deployment cycles
- Simpler access control
- Team autonomy
Common Misconceptions Debunked
❌ Monorepo ≠ Monolith ❌ Monorepos aren't just for Google/Facebook ❌ Monorepos aren't slower (they're often faster!) ❌ You don't lose modularity ❌ Git handles large repos well
Critical Success Factors
For Monorepo:
- Use proper tooling (Nx, Turborepo)
- Enable build caching
- Use affected commands
- Enforce module boundaries
- Plan organization upfront
For Polyrepo:
- Standardize tooling across repos
- Automate dependency updates
- Strong conventions and patterns
- Efficient code sharing mechanism
- Good CI/CD orchestration
What's Next?
Congratulations! You now understand the fundamental difference between monorepo and polyrepo strategies. You know when to use each approach and how major companies have solved these problems at scale.
Ready to try it yourself?
In the next article, we'll get hands-on and create your first Nx monorepo workspace. You'll learn:
- How to set up Nx from scratch
- Creating your first applications
- Understanding the workspace structure
- Running and building applications
- Exploring the generated files
Continue learning:
📚 Next: Nx Workspace: Creating Your First Monorepo
📚 Related topics:
Check Your Understanding
Test what you've learned with these questions. Think about them carefully before revealing the answers.
Quick Quiz
1. What's the main difference between monorepo and monolith?
<details> <summary>Show Answer</summary>
Monorepo is a code organization strategy - multiple projects in one repository.
Monolith is an architectural pattern - one large application.
They're completely different concepts! You can have:
- Microservices in a monorepo ✅
- A monolith in a polyrepo ✅
- Any combination
The repository strategy (mono/poly) is independent of the architecture (monolith/microservices).
</details>
2. When would polyrepo be better than monorepo?
<details> <summary>Show Answer</summary>
Polyrepo is better when:
- Projects are truly independent - No code sharing, different products
- Strict security isolation needed - Open source vs proprietary, different clearance levels
- Very different tech stacks - Python + Go + Swift + Java with incompatible tooling
- Large organization with autonomous teams - 100+ developers, teams rarely collaborate
- Different lifecycles - Legacy systems vs modern apps, very different release schedules
If your projects share code and need to change together, monorepo is usually better.
</details>
3. Why are monorepos often faster than polyrepos?
<details> <summary>Show Answer</summary>
Monorepos with modern tooling (like Nx) are faster because of:
-
Affected detection - Only builds/tests what actually changed
- Polyrepo: Runs full build even for tiny changes
- Monorepo: Builds only affected projects
-
Computation caching - Reuses results from previous builds
- Change something? If other projects unchanged, use cached builds (instant!)
-
No publishing overhead - Direct imports instead of npm publish/install cycle
- Polyrepo: Publish package, update deps, wait for CI (10-30 min)
- Monorepo: Change code, import it (instant)
-
Distributed execution - Can split work across multiple CI machines
- Parallel builds on different projects simultaneously
Real-world result: 20-80x faster for incremental builds!
</details>
4. What's wrong with this monorepo structure?
// apps/web/src/api-client.ts
import { getUserData } from "../../../apps/api/src/services/user";
<details> <summary>Show Answer</summary>
Multiple problems:
-
App-to-app dependency - Web app directly imports from API app
- Apps should be independent
- This creates tight coupling
-
Breaks modularity - Can't deploy apps independently
- If API changes, web must change
- Can't test web without API
-
Wrong import pattern - Using relative paths across app boundaries
- Hard to refactor
- Breaks encapsulation
Correct approach:
// Extract shared code to library
// libs/api-client/src/user-service.ts
export function getUserData() { ... }
// Use in web app
// apps/web/src/components/profile.tsx
import { getUserData } from '@workspace/api-client';
Shared code goes in libraries, not in apps!</details>
Hands-On Exercise
Challenge: Analyze your current project structure
Task:
- List all your current Git repositories
- For each pair of repos, estimate how much code they share (0-100%)
- Count how many times per month changes span multiple repos
- Identify which repos could be grouped together
Questions to consider:
- Would your team benefit from a monorepo?
- Which projects would you group together?
- What challenges might you face?
- What would be your first step?
Reflection: There's no single right answer! The goal is to think critically about your specific situation and make an informed decision.
Version Information
Tested with:
- Git: v2.40+
- Node.js: v18.x, v20.x, v22.x
- npm: v9.x, v10.x
- Nx: v19.x (covered in next articles)
Last updated: November 2024
Note: Monorepo concepts are tool-agnostic. The principles in this article apply whether you use Nx, Turborepo, Lerna, or Bazel.