One Codebase, Multiple Products: How Monorepos Accelerate Teams
Splitting a product into separate repositories feels clean until you need to share types, coordinate releases, or run a single CI pipeline. Monorepos solve this — here's when to make the move and how to structure it so it stays manageable as the team grows.
As codebases grow and teams scale, the way you organize your repositories becomes a critical architectural decision. Monorepos have emerged as a powerful pattern adopted by tech giants like Google, Meta, and Microsoft. In this guide, I'll explain what monorepos are, when to use them, and how they compare to traditional multi-repository approaches.
What is a Monorepo?
A monorepo is a single repository containing multiple distinct projects with well-defined relationships. The key word here is "distinct." A monorepo is not just throwing all your code into one folder. It's a structured approach where each project maintains its own boundaries while sharing a common infrastructure.
Think of it as an apartment building: each apartment (project) is self-contained with its own layout, but they all share the same foundation, utilities, and building management.
Monorepo vs Monolith vs Polyrepo
These terms are often confused, but they represent fundamentally different concepts:
POLYREPO MONOREPO MONOLITH
repo-frontend/ my-company/ my-app/
├── package.json ├── apps/ ├── package.json
└── src/ │ ├── web/ └── src/
│ ├── mobile/ ├── frontend/
repo-backend/ │ └── api/ ├── backend/
├── package.json ├── packages/ ├── shared/
└── src/ │ ├── ui/ └── utils/
│ └── utils/
repo-shared/ ├── package.json (tightly coupled,
├── package.json └── turbo.json no clear boundaries)
└── src/
(multiple projects,
(separate repos, clear boundaries,
independent releases) shared tooling)
Polyrepo (Multiple Repositories)
- Each project lives in its own repository
- Independent versioning and releases
- Requires publishing packages to share code
- Cross-project changes require multiple PRs
Monorepo (Single Repository, Multiple Projects)
- All projects in one repository with clear boundaries
- Shared tooling and configuration
- Direct imports between projects (no publishing)
- Atomic commits across multiple projects
Monolith (Single Codebase, No Boundaries)
- Everything in one tightly coupled codebase
- No clear separation between components
- Changes in one area can break unrelated features
- Difficult to scale teams independently
The Problems with Polyrepos
Before understanding why monorepos exist, let's examine the friction points in traditional multi-repository setups:
Code Sharing Overhead
Want to share a utility function between two projects? In a polyrepo world, you need to:
- Create a new repository for the shared package
- Set up build and publish pipelines
- Configure npm/package registry access
- Manage semantic versioning
- Update consumers when changes are made
This overhead discourages sharing, leading to code duplication across repositories.
Cross-Repository Changes
Imagine finding a bug in a shared library. In a polyrepo setup:
- Fix the bug in the library repository
- Create a PR, wait for review, merge
- Publish a new version
- Update the dependency in every consuming repository
- Create PRs for each consumer, wait for reviews, merge
A simple bug fix becomes a multi-day coordination effort.
Inconsistent Tooling
Each repository evolves independently, leading to:
- Different linting rules and configurations
- Varying test frameworks and conventions
- Inconsistent build processes
- Different CI/CD pipelines to maintain
Dependency Hell
Different projects may depend on different versions of the same library, causing:
- Compatibility issues when integrating
- Security vulnerabilities in outdated dependencies
- Increased bundle sizes from duplicate dependencies
Benefits of Monorepos
Monorepos address these challenges through unified infrastructure and atomic changes.
Zero-Overhead Code Sharing
Creating a shared package is as simple as creating a new folder. No publishing infrastructure, no version management for internal code. Just import and use.
import { Button } from "@repo/ui";
import { formatDate } from "@repo/utils";
Atomic Commits
Change a shared component and update all consumers in a single commit. No coordination across repositories, no version bumps, no waiting for publishes.
git commit -m "refactor: update Button API across all apps"
Single Source of Truth
- One ESLint configuration for all projects
- One TypeScript configuration to maintain
- One CI/CD pipeline that handles everything
- One place to look for any piece of code
Simplified Dependency Management
All projects use the same version of external dependencies. Update React once, and every project gets the update. No more "which version of lodash does project X use?"
Improved Collaboration
- Developers can easily contribute to any project
- Code review catches cross-project issues
- Shared ownership reduces silos
- New team members onboard faster with unified tooling
Large-Scale Refactoring
Rename a function used across 50 files in 10 projects? In a monorepo, it's a single find-and-replace operation with one PR. In polyrepos, it's weeks of coordination.
Challenges of Monorepos
Monorepos are not without trade-offs. Understanding these challenges helps you make an informed decision.
Tooling Complexity
Standard tools like npm and git were designed for single-project repositories. Monorepos require specialized tooling:
- Build orchestration (Turborepo, Nx, Bazel)
- Workspace management (npm/yarn/pnpm workspaces)
- Selective CI/CD (only build what changed)
Repository Size
As the repository grows:
- Git operations slow down
- Clone times increase
- IDE performance may suffer
Solutions include shallow clones, sparse checkouts, and tools like Git LFS for large files.
Access Control
Traditional Git hosting provides repository-level permissions. In a monorepo:
- Everyone can see all code (may be a security concern)
- Fine-grained permissions require additional tooling
- CODEOWNERS files help but have limitations
Build Times
Without proper tooling, every change triggers a full rebuild of everything. This is where build systems like Turborepo become essential, providing:
- Incremental builds (only rebuild what changed)
- Caching (never rebuild the same thing twice)
- Parallel execution (use all CPU cores)
When to Use a Monorepo
Monorepos shine in specific scenarios:
Good Fit
- Multiple related applications: A web app, mobile app, and API that share types and utilities
- Component libraries: UI components consumed by multiple applications
- Microservices with shared contracts: Services that need consistent API types
- Full-stack applications: Frontend and backend that share validation logic
- Teams that collaborate frequently: Cross-functional teams working on interconnected features
Poor Fit
- Unrelated projects: Projects with no shared code or dependencies
- Different release cycles: Projects that need completely independent versioning
- Strict access control: When teams absolutely cannot see each other's code
- Legacy migration: Forcing unrelated legacy projects together
Monorepo Tools Comparison
Several tools exist to manage monorepos effectively. Here's how the major players compare:
Turborepo
- Focus: JavaScript/TypeScript
- Strengths: Simple setup, excellent caching, Vercel integration
- Best for: JS/TS projects wanting quick adoption
Nx
- Focus: JavaScript/TypeScript with plugin ecosystem
- Strengths: Rich features, code generation, dependency graph visualization
- Best for: Enterprise teams wanting comprehensive tooling
Bazel
- Focus: Polyglot (any language)
- Strengths: Distributed execution, hermetic builds, massive scale
- Best for: Large organizations with diverse tech stacks
Lerna
- Focus: JavaScript package publishing
- Strengths: Package versioning, changelog generation
- Best for: Publishing multiple npm packages
Rush
- Focus: Enterprise TypeScript
- Strengths: Strict policies, phantom dependency prevention
- Best for: Large TypeScript codebases with strict requirements
Essential Monorepo Features
When evaluating monorepo tools, look for these capabilities:
Performance
- Local caching: Don't rebuild unchanged packages
- Remote caching: Share build artifacts across team and CI
- Parallel execution: Run independent tasks simultaneously
- Affected detection: Only run tasks for changed packages
Developer Experience
- Dependency graph: Understand package relationships
- Task orchestration: Define task dependencies declaratively
- Consistent commands: Same scripts work across all packages
- Watch mode: Rebuild on file changes during development
Maintainability
- Code generation: Scaffold new packages consistently
- Constraints: Enforce architectural boundaries
- Workspace analysis: Detect circular dependencies
- Migration tools: Upgrade dependencies across all packages
Monorepo Best Practices
Structure Your Workspace Clearly
my-monorepo/
├── apps/ # Deployable applications
│ ├── web/
│ ├── mobile/
│ └── api/
├── packages/ # Shared libraries
│ ├── ui/
│ ├── utils/
│ └── config/
└── tooling/ # Build and dev tools
├── eslint/
└── typescript/
Define Clear Package Boundaries
Each package should have a single responsibility. Avoid creating "utils" packages that become dumping grounds. Instead:
@repo/date-utilsfor date formatting@repo/validationfor form validation@repo/api-clientfor API communication
Use Consistent Naming
Adopt a naming convention for internal packages:
{
"name": "@repo/ui",
"name": "@repo/utils",
"name": "@repo/config-eslint"
}
Enforce Boundaries
Prevent packages from importing things they shouldn't:
- Apps can import from packages
- Packages should not import from apps
- UI packages should not import backend code
Invest in CI/CD
- Enable remote caching to speed up CI builds
- Use affected detection to only test changed packages
- Parallelize independent tasks
- Cache dependencies between CI runs
Conclusion
Monorepos are not a silver bullet, but they offer compelling advantages for teams building interconnected applications. The key takeaways:
- Monorepo does not equal monolith: Well-structured monorepos maintain clear boundaries between projects.
- Code sharing becomes trivial: No publishing infrastructure needed for internal packages.
- Atomic changes reduce coordination: Update shared code and consumers in one commit.
- Tooling is essential: Invest in build systems like Turborepo or Nx to manage complexity.
- Not for everyone: Evaluate whether your projects actually benefit from shared infrastructure.
If your team is building multiple related applications with shared code, a monorepo can dramatically improve developer productivity and code quality. Start small, invest in tooling, and let the benefits compound as your codebase grows.
Written by

Technical Lead and Full Stack Engineer leading a 5-engineer team at Fygurs (Paris, Remote) on Azure cloud-native SaaS. Graduate of 1337 Coding School (42 Network / UM6P). Writes about architecture, cloud infrastructure, and engineering leadership.