Monorepos with Git: Strategies, Tools & Tradeoffs
In this tutorial, you'll learn about Monorepos with Git: Strategies, Tools & Tradeoffs. We cover key concepts, practical examples, and best practices.
A monorepo stores multiple related projects in one Git repository, simplifying dependency management and enabling cross-project refactoring at scale.
In this tutorial, you'll learn monorepo strategies for managing multiple projects in a single Git repository. Monorepos simplify dependency management, enable cross-project refactoring, and streamline CI/CD — but they also introduce challenges with scale, code ownership, and git performance. By the end, you'll know how to structure, build, and maintain a monorepo effectively.
flowchart TD
subgraph MonorepoStructure
A[packages/] --> B[web-app]
A --> C[mobile-app]
A --> D[shared-lib]
A --> E[api-server]
B --> F[package.json]
C --> F
D --> F
end
subgraph CI
G[Affected: web-app] --> H[Test web-app only]
G --> I[Build web-app only]
end
Why Monorepos?
Monorepos solve several problems that arise with multiple repositories:
- Shared code: A bugfix in a shared library is applied in one commit across all projects
- Atomic commits: A feature that touches both frontend and backend is a single commit
- Unified CI/CD: One pipeline configuration, one build process
- Simplified dependency management: No version mismatch between packages
- Cross-team refactoring: Rename a shared interface and fix all consumers in one PR
Monorepo Structure
A typical monorepo layout:
my-monorepo/
├── packages/
│ ├── web-app/
│ │ ├── src/
│ │ └── package.json
│ ├── mobile-app/
│ │ ├── src/
│ │ └── package.json
│ └── shared-lib/
│ ├── src/
│ └── package.json
├── tools/
│ └── scripts/
├── package.json
├── turbo.json
└── .gitignore
Dependency Management
Use workspaces to manage shared dependencies:
// root package.json
{
"workspaces": [
"packages/*]
]
}
Install dependencies once at the root — all packages share them:
npm install
Expected output:
added 1250 packages in 15s
Build Tools for Monorepos
Turborepo
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
}
}
}
Run commands across packages:
npx turbo run test --filter=web-app
Nx
npx nx affected:test --base=main
Nx calculates which projects were affected by changes since main and only runs tasks on those.
Git Strategies for Monorepos
Use sparse checkout to work with only the packages you need:
git clone --filter=blob:none monorepo
git sparse-checkout set packages/web-app packages/shared-lib
This reduces clone time and disk usage significantly for large monorepos.
CODEOWNERS for Access Control
# .github/CODEOWNERS
/packages/web-app/ @team-web
/packages/api-server/ @team-api
/shared-lib/ @team-core
Pull requests touching these paths automatically request reviews from the respective teams.
Comparison: Monorepo vs Polyrepo
| Aspect | Monorepo | Polyrepo |
|---|---|---|
| Dependency updates | Single version across projects | Each repo manages own deps |
| Cross-project refactoring | One commit across all | Multiple PRs across repos |
| CI/CD complexity | Needs affected-project detection | Simple per-repo pipelines |
| Code ownership | Needs CODEOWNERS | Natural by repo |
| Git performance | Degrades with size | Stays constant |
| Tooling support | Specialized tools needed | Standard Git workflow |
| Atomic changes | Yes | No |
Common Errors
| Error | Cause | Fix |
|---|---|---|
npm ERR! ERESOLVE |
Conflicting dependencies across packages | Use resolutions or upgrade deps |
Too many open files on clone |
Large monorepo | Use shallow clone or sparse checkout |
Input/output error on git status |
Repository too large | Enable feature.manyFiles in Git config |
| CI runs all tests unnecessarily | No affected-project detection | Use Nx, Turborepo, or lerna changed |
Package not found in workspace |
Missing workspace config | Check package.json workspaces field |
Merge conflicts in package-lock.json |
Multiple teams modifying deps | Use pnpm with strict lockfile |
Pre-commit hook too slow |
Hook scans entire repo | Scope hook to changed files only |
| Git blame shows reformatting | Global formatting changes | Use .git-blame-ignore-revs |
Practice Questions
Challenge
Set up a monorepo with three packages: a shared library, a web application, and a CLI tool. Configure npm workspaces. Add Turborepo with build, lint, and test tasks. Implement CODEOWNERS so different teams own different packages. Demonstrate an atomic change across the shared library and the web app in a single commit.
Real-World Task
Migrate a project from three separate Git repositories into a single monorepo. Preserve the commit history of each repository using git subtree or git filter-repo. Set up Nx to detect affected projects and run only relevant CI pipelines. Configure sparse checkout for developers who only need specific packages. This monorepo migration approach is used at DodaTech for consolidating tools like DodaZIP and Durga Antivirus Pro components.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro