Day 42: Monorepo Management (Turborepo / Nx Setup & CI Strategy)
Structure a multi-package monorepo with correct dependency graphs and CI caching so builds/tests only run for what actually changed.
Study
Concepts
Why a monorepo, and the dependency graph it is built on
A monorepo hosts multiple packages (a web app, a mobile app, a shared UI library, a shared utils package) in one repository with one version history, making cross-package changes (e.g. updating a shared component and its two consumers) a SINGLE atomic commit/PR instead of a coordinated multi-repo release dance. The tradeoff is tooling complexity: without care, every CI run rebuilds and retests every package regardless of what actually changed, which does not scale.
Turborepo and Nx both work by building an explicit DEPENDENCY GRAPH from each package's manifest (`package.json` workspace dependencies), so they know that changing `packages/ui` requires rebuilding `apps/web` and `apps/mobile` (which depend on it) but NOT rebuilding an unrelated `packages/analytics` package that does not depend on `ui` at all.
Caching turns CI cost from "all packages" into "only what changed"
Both tools cache the OUTPUT of each task (build, test, lint) keyed by a hash of that package's relevant inputs (source files, dependencies, lockfile, task config) — if the hash matches a previous run (locally or, with remote caching, on a teammate's machine or in CI), the cached output is replayed instantly instead of re-executing the task. This is what makes "only rebuild what changed" a reality rather than an aspiration: a CI run touching one package can be seconds instead of minutes because every unrelated package's tasks are cache hits.
A well-designed CI strategy pairs this with `turbo run build --filter=...[HEAD^]` (or Nx's `affected` commands) style incremental targeting — computing exactly which packages were affected by the changed files in a PR, and running tasks ONLY for those and their dependents, rather than filtering after the fact.
See It
Visualizations
Visualization
Dependency graph drives what CI actually needs to run
Build It
Code Examples
Turborepo pipeline config — declaring task dependencies
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}
// "^build" means: build this package's DEPENDENCIES first, in graph order —
// Turborepo computes that order automatically from workspace package.json files.CI: only run tasks for packages affected by this PR
# .github/workflows/ci.yml (excerpt)
- name: Build & test only what changed
run: |
npx turbo run build test lint \
--filter=...[origin/main] \
--cache-dir=.turbo
# --filter=...[origin/main] selects: packages changed vs main,
# PLUS everything that depends on them (the "..." prefix) —
# exactly matching the dependency graph from the diagram above.Remember
Key Takeaways
- A monorepo trades multi-repo coordination overhead for tooling complexity — caching and graph-awareness are what make it scale.
- Turborepo/Nx build an explicit dependency graph from workspace package.json files to know what depends on what.
- Task outputs are cached by an input hash — unchanged packages replay cached results instantly instead of re-running.
- --filter=...[origin/main] (or Nx affected) targets exactly the changed packages plus their dependents, not the whole repo.
- This is a real, common senior-interview system design question: "how would you keep CI fast as a monorepo grows to 50 packages".
Do It
Practice
- 1Set up a minimal Turborepo with 3 packages (ui, web, mobile) where web and mobile depend on ui, and confirm changing ui triggers rebuilds of both.
- 2Run turbo run build twice with no changes and confirm the second run is a full cache hit (near-instant).
- 3Write the --filter command that would correctly target only affected packages for a PR compared against the main branch.