Stage 2 · Tools
Branching Strategies
Monorepo Strategies
Manage multiple services, packages, and apps in a single repository with scoped commits and path-based CI.
What Is a Monorepo?
A monorepo is a single Git repository that contains multiple projects, services, or packages. Instead of splitting your frontend, backend, shared libraries, and infrastructure code into separate repositories, they all live together in one repository with a shared history.
Companies like Google, Meta, Twitter, and Uber use monorepos. Google's monorepo contains over 80 billion lines of code across millions of files. The approach scales when you have the right tooling.
A monorepo is a repository strategy, not an architecture strategy. You can have a monorepo with microservices. Each service lives in its own directory, has its own tests, and can be deployed independently. The monorepo is just where the code lives.
Monorepo vs Polyrepo
The alternative to a monorepo is polyrepo — each project in its own Git repository. The choice has significant implications for how you manage dependencies, run tests, and coordinate changes.
| Aspect | Monorepo | Polyrepo |
|---|---|---|
| Cross-project changes | Single commit, atomic | Multiple repos, manual coordination |
| Dependency management | Direct imports, no versioning | Published packages, version bumps |
| Code sharing | Copy or import from relative path | npm packages, Go modules |
| CI complexity | Path-based triggers required | Per-repo pipelines |
| Access control | All or nothing (unless using tools) | Per-repo permissions |
| History visibility | Full cross-project history | Fragmented across repos |
| Tooling requirement | Build tools (Nx, Turborepo, Bazel) | Standard Git |
Scoped Commits and Path Logging
In a monorepo, a single commit might touch files in five different packages. Scoped commits and path-based logging help you understand what changed and where.
# See commits that touched a specific package
git log --oneline -- apps/web/
# See commits that touched the shared library
git log --oneline -- packages/shared/
# See commits in a date range for a specific path
git log --oneline --since="2024-01-01" -- packages/api/
# See who changed a specific file
git log --follow --format="%h %an %s" -- packages/ui/src/Button.tsx
# See the diff for a specific path in a commit
git show --stat abc1234 -- apps/web/
git diff abc1234..def5678 -- packages/shared/The -- separator tells Git to treat everything after it as a path. This lets you filter history by directory, package, or file — essential for navigating monorepo history.
# Format: <scope>: <description>
# Scope maps to the directory/package that changed
git commit -m "feat(web): add user dashboard page"
git commit -m "fix(api): handle null response from payment provider"
git commit -m "chore(shared): update TypeScript to 5.3"
git commit -m "docs(infra): update deployment guide"
git commit -m "test(ui): add button component unit tests"Scoping commits to the package or directory that changed makes git log and git blame more useful. You can grep for scope prefixes to find all changes to a specific package.
Use commitlint with a scope list that matches your monorepo directories. This prevents commits like fix: stuff and ensures every commit clearly identifies which package was modified.
Path-Based CI Triggers
In a monorepo, you do not want to run every test suite on every commit. If you change only apps/web/, you should not rebuild packages/api/. Path-based CI triggers solve this by detecting which paths changed and only running relevant pipelines.
name: CI
on:
push:
branches: [main]
paths:
- "apps/web/**"
- "packages/shared/**"
pull_request:
branches: [main]
paths:
- "apps/web/**"
- "packages/shared/**"
jobs:
web-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test --workspace=apps/web
- run: npm run build --workspace=apps/webThe paths filter tells GitHub Actions to only run this workflow when files in apps/web/ or packages/shared/ are modified. Changes to packages/api/ or infrastructure/ do not trigger this pipeline.
# Each package gets its own workflow with path triggers
# .github/workflows/api.yml
name: API Tests
on:
push:
paths: ["packages/api/**", "packages/shared/**"]
# .github/workflows/web.yml
name: Web Tests
on:
push:
paths: ["apps/web/**", "packages/shared/**"]
# .github/workflows/infra.yml
name: Infrastructure
on:
push:
paths: ["infrastructure/**"]Each package has its own CI workflow. When a commit touches multiple packages, multiple workflows run in parallel. When a commit touches one package, only that package's workflow runs.
Monorepo Tools
Managing a monorepo at scale requires tooling for task orchestration, dependency graph analysis, caching, and affected-based testing.
| Tool | Language | Key Feature | Caching |
|---|---|---|---|
| Nx | TypeScript/JS | Affected commands, dependency graph | Local + remote |
| Turborepo | TypeScript/JS | Parallel execution, task hashing | Local + remote |
| Bazel | Any | Hermetic builds, remote execution | Remote |
| Lerna | TypeScript/JS | Package publishing, versioning | Delegates to Nx |
| Rush | TypeScript/JS | Monorepo package manager | Built-in |
# Install Nx
npm install -g nx
# See the dependency graph
nx graph
# Run tests only for packages affected by your changes
nx affected --target=test
# Build only affected packages
nx affected --target=build
# Lint only changed files
nx affected --target=lintnx affected compares your branch against main, determines which packages were modified, and only runs tasks on those packages. This is the primary efficiency gain of monorepo tooling.
# Install turbo
npm install -g turbo
# Run a task across all packages
turbo run build
# Run with caching (reuses previous results)
turbo run test --cache
# Run only in packages that changed
turbo run build --filter=...[origin/main]
# Run a task and its dependents
turbo run build --filter=sharedTurborepo hashes input files to determine cacheability. If packages/shared has not changed since the last build, Turborepo skips rebuilding it. Remote caching lets your CI server share cache across runs.
Tradeoffs and Decisions
Monorepos are not always the right choice. The decision depends on your team size, deployment model, and tooling investment.
Monorepos work best when packages share code tightly, cross-project changes are frequent, and you need atomic commits across boundaries. If your team makes weekly cross-cutting changes, a monorepo eliminates the coordination overhead of multi-repo PRs.
- Monorepo advantages: atomic cross-package changes, shared history, simplified dependency management.
- Monorepo disadvantages: requires tooling investment, CI complexity, all-or-nothing access control.
- Start with a monorepo if you are building a new project with shared code between frontend and backend.
- Use path-based CI triggers to avoid running every test on every commit.
- Adopt Nx or Turborepo early — retrofitting build tools onto an existing monorepo is painful.
- Consider polyrepo when packages have independent lifecycles or different access control needs.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.