Summary
A monorepo stores every package — apps, libs, design tokens — in a single git repository. Turborepo and Nx add task orchestration with local and remote output caching, so a build that already ran never runs again. The payoff: atomic cross-package changes, shared tooling, and CI times that don't scale with repo size.
Jump to the interview angleA monorepo is not a monolith. Each package keeps its own package.json, build output, and public API. Shared across all packages: one lockfile, one linting config, one task runner.
Turborepo models tasks as a DAG. A task runs only after its dependsOn tasks succeed; ^build means "build every dependency package first." Outputs are hashed against inputs — a cache hit replays them without re-executing.
Nx centers on a project graph — a computed map of every package and its import edges. nx affected -t build walks the graph from changed files and builds only transitive consumers.
How the tooling works
- `pnpm-workspace.yaml` declares package directories; `workspace:*` pins a dep to the local copy.
- Turborepo hashes all task inputs; a matching hash restores cached outputs without re-running.
- `turbo login && turbo link` opts the repo into Vercel's remote cache, sharing hits across machines.
- Nx builds its project graph from import analysis; `nx graph` renders it visually.
- Internal packages (`packages/ui`, `packages/types`) are imported by name — no publish step needed.
- Nx module boundary rules and explicit `exports` fields block invisible cross-package coupling.
turbo.json — build pipeline with remote-cache-friendly outputs
dependsOn: ["^build"] builds every upstream package first. Listing outputs is required — omit it and Turbo caches nothing for that task.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"cache": true
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"cache": true
},
"dev": {
"cache": false,
"persistent": true
}
}
}Excluding .next/cache/** from outputs keeps the cached artifact small without busting the cache.
Monorepo tradeoffs
Pros
- Atomic cross-package refactors land in one commit with one CI run.
- Shared tooling (ESLint, TypeScript, Prettier config) is updated once and propagates everywhere.
- Task caching means CI time grows logarithmically, not linearly, as packages accumulate.
- A single lockfile eliminates version drift between apps sharing the same dependency.
- Internal packages ship zero overhead — no npm publish, no version bump, just import.
Cons
- Startup cost is real: pnpm workspace setup, turbo.json, path aliases, and CI configuration all need upfront work.
- Remote cache misconfigurations silently fall back to full rebuilds — hard to diagnose.
- A shared lockfile means one package's dependency upgrade affects every app in the repo.
- Nx project graph inference can misread dynamic imports or re-exports, producing a wrong affected set.
- Permissions and ownership are harder: one repo means one set of git access controls.
Interview angle
Expect questions on why a monorepo helps at scale, how caching works, and the tradeoff vs. polyrepo. The sharp follow-up is "how do you stop it from becoming a distributed monolith?"
Soundbite: "Turborepo caches task outputs by hashing inputs — if nothing changed, the output is replayed, not recomputed. Remote caching extends that to every developer and every CI runner."
Key terms
- turbo.json tasks
- Turborepo's DAG of named tasks with dependsOn, outputs, and cache settings.
- ^dependsOn
- Caret prefix: run this task in all upstream dependency packages first.
- remote cache
- Shared artifact store so a Turbo/Nx cache hit works across machines and CI.
- nx affected
- Runs a task only on packages changed since a base Git ref, plus their dependents.
- workspace: protocol
- pnpm syntax pinning a dependency to the local workspace package, not the registry.