Skip to main content
Technology & EngineeringGit Workflow145 lines

Monorepo Management

Monorepo strategies with Nx and Turborepo for scalable multi-project repositories

Quick Summary28 lines
You are an expert in monorepo strategies using tools like Nx and Turborepo for scalable multi-project repositories.

## Key Points

- Atomic cross-project changes in a single commit
- Shared tooling, lint rules, and CI configuration
- Simplified dependency management between internal packages
- Enable remote caching (Nx Cloud or Turborepo Remote Cache) so CI and teammates reuse build artifacts instead of rebuilding unchanged projects
- Define clear ownership with a CODEOWNERS file mapping each `apps/` and `packages/` directory to the responsible team
- Use affected commands in CI to avoid running the entire test suite on every pull request
- Not fetching full git history in CI (`fetch-depth: 0`), which prevents affected-change detection from finding the merge base
- Allowing circular dependencies between packages, which breaks incremental builds and caching

## Quick Example

```bash
# Nx — only test projects affected by changes since main
npx nx affected -t test --base=main

# Turborepo — run tests with dependency-aware filtering
npx turbo run test --filter=...[origin/main]
```

```bash
npx create-turbo@latest
```
skilldb get git-workflow-skills/Monorepo ManagementFull skill: 145 lines
Paste into your CLAUDE.md or agent config

Monorepo Management — Git Workflows

You are an expert in monorepo strategies using tools like Nx and Turborepo for scalable multi-project repositories.

Overview

A monorepo is a single repository containing multiple distinct projects or packages. Tools like Nx and Turborepo add dependency-aware task execution, remote caching, and affected-change detection to keep builds fast as the repository grows.

Core Concepts

Why monorepos:

  • Atomic cross-project changes in a single commit
  • Shared tooling, lint rules, and CI configuration
  • Simplified dependency management between internal packages

Workspace structure (typical):

repo/
  apps/
    web/
    api/
  packages/
    ui/
    utils/
    config-eslint/
  package.json          # root workspace config
  nx.json / turbo.json  # orchestration config

Affected-change detection:

# Nx — only test projects affected by changes since main
npx nx affected -t test --base=main

# Turborepo — run tests with dependency-aware filtering
npx turbo run test --filter=...[origin/main]

Implementation Patterns

Nx setup:

npx create-nx-workspace@latest myorg --preset=ts
# Add a project
npx nx generate @nx/node:application api
# Run a target
npx nx run api:build
# Visualize the dependency graph
npx nx graph
// nx.json
{
  "targetDefaults": {
    "build": { "dependsOn": ["^build"], "cache": true },
    "test":  { "cache": true }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts"]
  }
}

Turborepo setup:

npx create-turbo@latest
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    }
  }
}

CI with affected detection (GitHub Actions):

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx nx affected -t lint test build --base=origin/main

Core Philosophy

A monorepo is a bet that the benefits of atomic cross-project changes, shared tooling, and unified CI outweigh the costs of increased repository size, more complex build orchestration, and tighter coupling between teams. This bet pays off when projects in the repository genuinely share dependencies, APIs, or release cycles. It fails when unrelated projects are colocated simply because "we have a monorepo" — adding build complexity and coupling without the cross-project coordination benefits that justify it. The decision to monorepo should be driven by actual dependency relationships, not organizational convention.

Build performance in a monorepo is solved by two complementary techniques: affected-change detection and remote caching. Affected detection (Nx's affected, Turborepo's --filter) answers "which projects could possibly be impacted by this change?" and runs only those tasks. Remote caching answers "has someone already run this exact task with this exact input?" and reuses the output. Together, they make CI times proportional to the scope of the change rather than the size of the repository. Without both, monorepo CI times grow linearly with the number of projects and eventually become a bottleneck that negates the workflow benefits.

The dependency graph is the monorepo's most important artifact. Tools like Nx and Turborepo build a graph of which packages depend on which other packages and use it to determine task ordering, parallelization, and affected-change detection. This graph must be accurate — a missing dependency edge means a package is not rebuilt when its dependency changes, producing silent staleness. A circular dependency breaks the graph entirely and prevents incremental builds. Maintaining the dependency graph's integrity (no cycles, no missing edges) is a continuous discipline, not a one-time setup.

Anti-Patterns

  • Running all tasks on every change. Not using affected-change detection means every PR triggers builds and tests for every project in the repository, regardless of what changed. This wastes CI compute and makes PR feedback cycles unnecessarily slow. Use nx affected or turbo --filter=...[base] in CI.

  • Shallow clones in CI without base ref. Using fetch-depth: 1 in GitHub Actions prevents affected-change detection from finding the merge base with the main branch. Without the full history between the PR branch and main, affected cannot determine what changed and falls back to running everything. Use fetch-depth: 0 or fetch enough history to include the merge base.

  • Circular dependencies between packages. Package A depending on Package B which depends back on Package A breaks the dependency graph, prevents incremental builds, and makes task ordering impossible. Design packages with clear layering: shared utilities at the bottom, domain packages in the middle, applications at the top.

  • No CODEOWNERS for package directories. In a monorepo with multiple teams, changes to shared packages can merge without review from the teams that depend on them. Use a CODEOWNERS file that maps each packages/ and apps/ directory to the responsible team so changes require approval from the right people.

  • Local-only caching. Running Nx or Turborepo without remote caching means every CI run rebuilds from scratch even when the same task with the same inputs has already been computed by another developer or CI run. Enable remote caching so the work done by one person benefits everyone.

Best Practices

  • Enable remote caching (Nx Cloud or Turborepo Remote Cache) so CI and teammates reuse build artifacts instead of rebuilding unchanged projects
  • Define clear ownership with a CODEOWNERS file mapping each apps/ and packages/ directory to the responsible team
  • Use affected commands in CI to avoid running the entire test suite on every pull request

Common Pitfalls

  • Not fetching full git history in CI (fetch-depth: 0), which prevents affected-change detection from finding the merge base
  • Allowing circular dependencies between packages, which breaks incremental builds and caching

Install this skill directly: skilldb add git-workflow-skills

Get CLI access →