Most teams start with one repository per project—it's simple, familiar, and works until it doesn't. When you're coordinating deployments across a shared API and three frontend clients, or when every shared library update requires manual synchronization across repos, you start to wonder if there's a better way. Monorepos with Git offer one: keeping multiple projects in a single repository while maintaining clear ownership, fast builds, and atomic consistency.
What are monorepos and why production teams use them
A monorepo is a single Git repository containing multiple logically independent projects or packages. Unlike a monolith (one application), a monorepo houses many applications and libraries that can be built, tested, and deployed independently—but live under one roof.
We've seen this pattern emerge across e-commerce platforms managing API services, admin dashboards, and mobile clients; in SaaS products balancing backend services with multiple frontend experiences; and in trading systems where shared data models must stay synchronized across services. Google, Meta, Microsoft, and Twitter all operate monorepos at scale. The appeal isn't novelty; it's that certain coordination problems become easier to solve when you can version everything together.
A typical monorepo structure looks like this:
my-platform/
├── apps/
│ ├── api/
│ ├── web-dashboard/
│ └── mobile-client/
├── packages/
│ ├── shared-types/
│ ├── auth-library/
│ └── ui-components/
├── tools/
├── pnpm-workspace.yaml
└── nx.jsonEach subdirectory can have its own build process, tests, and deployment pipeline, but they all share version history, tooling configuration, and dependency management.
Benefits: Atomic changes and shared tooling
The first tangible win is atomic commits across projects. Imagine you're rolling out a breaking change to your shared authentication library. In a polyrepo setup, you'd need to:
- Update the library repo
- Release a new version
- Update the package in the API repo
- Test and deploy the API
- Update the package in the web app repo
- Test and deploy the web app
Each step is a separate commit, separate CI run, separate point of failure. In a monorepo, all those changes land in one commit:
git commit -m "refactor(auth): simplify session validation
- Update auth-library interface
- Update API service to new interface
- Update web app to new interface
- Update mobile client to new interface
All consumers updated atomically."Your CI runs once. You either deploy everything or nothing. No version mismatch surprises in production.
The second major benefit is unified tooling and standards. One eslint config, one TypeScript config, one testing framework, one deployment script. We've worked on teams where polyrepos led to drift: one service using Jest with coverage thresholds, another using Mocha, another skipping tests entirely. Monorepos make it much harder to accumulate this kind of technical debt because tooling lives in the root and is inherited by convention.
Developers also onboard faster. A new engineer clones one repo, installs dependencies once, and can understand and contribute to any project in it. Contrast this with a polyrepo where each project has its own setup, quirks, and local environment requirements.
Challenges: Repository size and CI performance
Monorepos solve coordination problems but create new ones. The most immediate is repository size.
A moderately-sized monorepo with a few years of history can easily hit 500MB to 2GB. Clone time becomes painful. History-heavy operations (bisect, blame, rebase) slow down. Windows developers particularly suffer—file path length limits and slow filesystem operations compound the problem. We've seen teams where `git clone` takes 20 minutes on a fresh machine.
The second challenge is CI time. With a polyrepo, changing the auth library triggers CI only on the auth library. With a monorepo, the default behavior is to run all tests, all linters, all builds. If you've got 15 independent projects sharing one CI pipeline, you're either:
- Waiting 45 minutes for every commit to be validated (unacceptable)
- Running only changed projects and their dependents (requires dependency analysis)
The second path is smarter but requires tooling.
A third subtle challenge is permission boundaries. In a polyrepo, you can restrict who can merge to the trading-system repo. In a monorepo, everyone with write access can touch everything. You have to enforce ownership through tooling and process, not Git permissions.
Tools: Nx, Turborepo, and Bazel for intelligent builds
Purpose-built monorepo tools solve the "run only what changed" problem through dependency analysis. Let's look at the three most common.
Nx
Nx (from Nrwl) is the most JavaScript-focused. It builds a dependency graph of your projects and understands what's affected by any given change. Running tests becomes:
nx affected:test --base=origin/mainNx only tests projects that changed and their dependents. It also provides caching: if a project built the same way before, Nx uses the cached output instead of rebuilding. On a team of six developers, this often cuts CI time by 60–75%.
Nx is particularly strong for React, Next.js, NestJS, and Node.js monorepos. The configuration is declarative:
{
"version": 2,
"projects": {
"api": {
"root": "apps/api",
"sourceRoot": "apps/api/src",
"targets": {
"build": {
"executor": "@nrwl/node:build"
}
}
}
}
}Turborepo
Turborepo (acquired by Vercel) is language-agnostic and lighter-weight. It's a task orchestrator built in Rust that's faster at analyzing and caching than Nx. Configuration is simpler—you define workspaces and tasks in turbo.json:
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
}
}
}
Turborepo works well if you have a heterogeneous monorepo (Python services alongside Node.js alongside Go microservices). Many teams find it easier to adopt than Nx because it's less opinionated.
Bazel
Bazel (from Google) is the heaviest tool and the most powerful. It's a build system that treats every artifact as a target with explicit dependencies. It provides perfect incremental builds and complete reproducibility—a given target always produces identical output if inputs are identical.
Bazel is best reserved for very large codebases (50+ projects) where build time is a constant pain. The learning curve is steep, and configuration is verbose. Most teams don't need it.
Git features: Sparse checkout and partial clone
Even with the right build tool, a 2GB repo is a 2GB repo. Git itself offers features to make monorepos more manageable.
Sparse checkout
Sparse checkout lets you clone the repository but only check out specific directories on disk. If you're working on the auth library, you don't need the mobile-client app files on your machine:
git clone --sparse https://github.com/my-org/platform.git
cd platform
git sparse-checkout set packages/auth-library apps/apiNow your working directory only contains those two directories. The full history is still available locally (you can still git log everything), but you're not wasting disk space or VS Code indexing on irrelevant projects. This is a huge quality-of-life improvement on large monorepos.
Partial clone
Partial clone goes further: it defers downloading blob objects (file contents) until you actually need them. The repository metadata is downloaded, but large files stay on the server until you check them out:
git clone --filter=blob:none https://github.com/my-org/platform.gitThis can reduce initial clone time from 10 minutes to 30 seconds. Combine sparse checkout and partial clone, and even a very large monorepo becomes manageable on any machine.
Common mistakes
- Adopting a monorepo without a build tool. If you're not using Nx, Turborepo, or similar, you're running full CI on every commit and defeating the purpose. Don't go monorepo without tooling to match.
- Treating a monorepo like a monolith. A monorepo should enable independent deployment and ownership. If everything is tightly coupled and must deploy together, you have a monolith and should consider splitting it up.
- Ignoring repository size. Sparse checkout and partial clone exist for a reason. If your repo is 5GB and you're not using these features, you're making your team slower than they need to be.
- Relaxing code review standards. In a polyrepo, merging to the wrong branch is hard (it's a different repo). In a monorepo, it's easy to merge a change that affects code you don't own. Enforce ownership through CODEOWNERS and required reviews.
- Skipping dependency analysis in CI. Running all tests on every commit defeats the monorepo's speed advantage. Use your tool's affected command or equivalent to run only what matters.
Key takeaways
- Monorepos enable atomic commits across projects and unify tooling and standards, but they introduce repository size and CI performance challenges.
- Nx and Turborepo solve the CI problem through dependency analysis and caching; Bazel is for extreme scale.
- Git's sparse checkout and partial clone make large monorepos practical to work with on any machine.
- Adopt a monorepo only if you have a build tool strategy and clear ownership boundaries.
- Start small: many successful monorepos began with 3–5 projects and grew from there once the team was comfortable with the tooling.






Comments
Loading comments…