Should your frontend, backend, shared libraries and infrastructure code live in one repository (monorepo) or several (polyrepo)? Both work at every scale: Google and Meta run giant monorepos, and many successful companies run hundreds of small repositories. The right choice depends on how your code changes together, and how your team works.
At a glance
| Monorepo | Polyrepo | |
|---|---|---|
| Cross-project changes | One commit / pull request | Several coordinated PRs and releases |
| Code sharing | Import directly | Publish packages, or use submodules |
| Dependency versions | Easy to keep consistent | Can drift between repos |
| CI | Needs “only build what changed” logic | Simple per repo |
| Access control | Coarser (whole repo, plus CODEOWNERS) | Per repo |
| Deploy independence | Possible, but needs discipline | Natural |
| Repo size and clone time | Grows large | Small |
| Tooling | Nx, Turborepo, pnpm/yarn workspaces, Bazel, Pants | Standard Git workflows |
Monorepo strengths
- Atomic changes: update an API and all its callers in one pull request.
- Easy refactoring and code reuse across projects.
- One place for tooling, linting, CI configuration and documentation.
- Simpler onboarding: clone once, see everything.
Monorepo costs
- CI must be smart: build and test only affected projects, or pipelines get slow. Path filters (
paths:in GitHub Actions,rules: changes:in GitLab CI) or tools like Nx/Turborepo do this. - Permissions: everyone with access sees everything. CODEOWNERS files control reviews, not reading.
- Size: large histories, and binary assets. Use shallow or partial clones, and Git LFS for big files. See Git LFS.
Polyrepo strengths
- Independence: each service has its own release cycle, CI, and permissions.
- Clear ownership boundaries for teams.
- Small, fast repositories.
Polyrepo costs
- Coordinated changes across repos are slow (version bumps, multiple PRs, release ordering).
- Duplicated configuration (CI, linting) drifts unless templated.
- Shared code needs a package registry (npm, PyPI, Composer, Go modules), or submodules. See Git submodules.
What fits small teams
For a small team building one product (web app + API + workers + infrastructure code):
- a monorepo is often simplest: one PR for a feature that touches the frontend and backend, one CI configuration, shared types;
- keep deployments independent with path-based CI triggers, and separate Docker images per service. See GitHub Actions Docker builds.
Polyrepos make more sense when parts are owned by different teams, have different access requirements (open-source library vs private app), or have truly independent lifecycles.
Example monorepo layout
.
├── apps/
│ ├── web/ # frontend
│ ├── api/ # backend service
│ └── worker/
├── packages/
│ ├── ui/ # shared components
│ └── types/ # shared TypeScript types
├── infra/
│ ├── ansible/
│ └── terraform/
└── .github/workflows/
CI example (GitHub Actions) building the API only when it changes:
on:
push:
paths:
- "apps/api/**"
- "packages/types/**"
See CI/CD pipeline explained and Linux configuration management.
Migrating
- Polyrepo → monorepo:
git subtree addorgit filter-repolet you import repositories with history into subdirectories. - Monorepo → polyrepo:
git filter-repo --subdirectory-filter apps/apiextracts one project with its history.
Quick reference
| If… | Prefer |
|---|---|
| One product, one small team | Monorepo |
| Shared types/libraries change often | Monorepo |
| Separate teams, separate release cycles | Polyrepo |
| Different access needs (public vs private) | Polyrepo |
| Monorepo CI too slow | Path filters, Nx/Turborepo/Bazel |
Related: Git branching strategies and deployment strategies.