Skip to main content

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:

  1. Update the type in one repository
  2. Publish it as an npm package
  3. Update the package version in three other repositories
  4. Wait for CI/CD to run in all four repos
  5. 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:

  1. shophub-web - Customer website
  2. shophub-mobile - Mobile shopping app
  3. shophub-api - Backend API
  4. shophub-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:

  1. Merge and deploy the API first
  2. Then merge and deploy all three frontends
  3. Coordinate timing to avoid breaking changes
  4. 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:

  1. Search across 4+ repositories
  2. Create 4+ pull requests
  3. Coordinate merging order
  4. Deploy in the right sequence
  5. Hope you didn't miss anything
  6. 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:

  1. Shared infrastructure code - Gmail and Google Drive can share authentication libraries
  2. Atomic changes - An API change in Search can update all consumers in one commit
  3. Code visibility - Engineers can see and learn from any team's code
  4. 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:

  1. Code sharing - Instagram can use Facebook's React components
  2. Cross-platform consistency - Types and logic shared between iOS/Android/Web
  3. Fast experimentation - Engineers can quickly prototype across products
  4. 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:

  1. Strong conventions - All services follow same patterns
  2. Shared libraries - Published to internal npm/Maven registries
  3. Standardized tooling - Same build tools across all repos
  4. Automated updates - Bots update dependencies across repos
  5. 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 &lt; 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)

  1. 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!
  1. 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
  1. 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

ApproachFull BuildRebuild After ChangePR Validation
Polyrepo (serial)40 min40 min (all repos)40 min
Polyrepo (parallel CI)15 min15 min (all repos)15 min
Monorepo (no cache)12 min12 min12 min
Monorepo (with Nx cache)12 min2 min (affected)2 min
Monorepo (Nx + Cloud)8 min30 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:

TaskPolyrepoMonorepoSavings
Add shared utility function30 min5 min83%
Update shared type definition45 min2 min96%
Refactor across 3 projects2 hours20 min83%
Onboard new developer4 hours1 hour75%
Run all tests locally25 min3 min88%

Source: Aggregated from Airbnb, Uber, and Microsoft case studies

CI/CD Cost Comparison

Monthly CI/CD costs for medium-sized project (10 engineers):

ApproachBuild Minutes/MonthCost (@$0.008/min)
Polyrepo50,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:

  1. Use proper tooling (Nx, Turborepo)
  2. Enable build caching
  3. Use affected commands
  4. Enforce module boundaries
  5. Plan organization upfront

For Polyrepo:

  1. Standardize tooling across repos
  2. Automate dependency updates
  3. Strong conventions and patterns
  4. Efficient code sharing mechanism
  5. 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:

  1. Projects are truly independent - No code sharing, different products
  2. Strict security isolation needed - Open source vs proprietary, different clearance levels
  3. Very different tech stacks - Python + Go + Swift + Java with incompatible tooling
  4. Large organization with autonomous teams - 100+ developers, teams rarely collaborate
  5. 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:

  1. Affected detection - Only builds/tests what actually changed

    • Polyrepo: Runs full build even for tiny changes
    • Monorepo: Builds only affected projects
  2. Computation caching - Reuses results from previous builds

    • Change something? If other projects unchanged, use cached builds (instant!)
  3. 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)
  4. 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:

  1. App-to-app dependency - Web app directly imports from API app

    • Apps should be independent
    • This creates tight coupling
  2. Breaks modularity - Can't deploy apps independently

    • If API changes, web must change
    • Can't test web without API
  3. 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:

  1. List all your current Git repositories
  2. For each pair of repos, estimate how much code they share (0-100%)
  3. Count how many times per month changes span multiple repos
  4. 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.