On this page · sections
Monorepo Build Pipeline
A monorepo keeps shared UI, config, and types in one place — one PR can update a button and every consumer. Interviewers test task graphs, caching, affected CI, and publish workflows.
| Tool | Focus | Best for |
|---|---|---|
| Workspaces | Link local packages | workspace:* dependencies |
| Turborepo | Task runner + cache | Lean config, Vercel remote cache |
| Nx | Full dev platform | Generators, boundaries, enterprise governance |
| Changesets | Version + publish | Semver for internal libraries |
Why Monorepo — Turborepo vs Nx
Definition
{
"name": "acme",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"test": "turbo run test"
}
}Three wins:
- Atomic cross-package changes — one PR updates API and all consumers
- Shared tooling once — ESLint, TypeScript, CI at root
- Cacheable task graphs — skip unchanged work via hashing
Turborepo — lean task runner + remote cache. Nx — project graph, generators, module boundaries, plugin ecosystem.
pnpm content-addressable storage and strict symlinks — common pairing; fewer phantom dependencies than npm.
Polyrepo still correct when teams are fully independent with semver-published packages and no overlapping release trains.
Prevent circular deps: apps → packages, never packages → apps. Nx @nx/enforce-module-boundaries or ESLint import/no-restricted-paths.
Interview: Monorepo ≠ one giant bundle — each package still builds independently.
Task Graph & dependsOn
Definition
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
}
}
}turbo run build order:
1. packages/ui build (no deps)
2. packages/utils build
3. apps/web build (waits for ^build)dependsOn: ["^build"]— runbuildin workspace dependencies first (caret = upstream)dependsOn: ["build"]— runbuildin the same package firstoutputs— folders to hash and cacheinputs— globs that invalidate cache
Without ^build, apps compile against stale @acme/ui dist. Persistent tasks (dev, watch) opt out of caching.
turbo run build --filter=@acme/web builds that package and dependency subgraph.
Interview:
dependsOn: ["build"]= same package.dependsOn: ["^build"]= dependency packages first.
Local & Remote Cache
Definition
Turborepo hashes task name, package, inputs, environment variables in env/globalEnv, and dependency task outputs. Same hash = cache hit.
{
"globalDependencies": ["pnpm-lock.yaml", "tsconfig.base.json"],
"globalEnv": ["NODE_ENV", "CI"],
"tasks": {
"build": {
"env": ["NEXT_PUBLIC_API_URL"],
"inputs": ["src/**", "package.json", "tsconfig.json"],
"outputs": ["dist/**"]
}
}
}- Local cache —
.turbo/cache - Remote cache — Vercel, S3, Nx Cloud — CI agents reuse prior builds
globalDependencies— root files invalidating everything
Never cache non-deterministic tasks (deploy, live API e2e). Candidates forget env vars in the hash — wrong cache hits when API_URL changes unlisted.
Missing outputs → cache hits return empty folders — silent broken builds.
Interview: Below 60% cache hit rate usually means misconfigured
inputsor over-broadglobalDependencies.
Affected-Only CI
Definition
# .github/workflows/ci.yml — simplified
jobs:
build:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run lint test build --filter=...[origin/main]Changed: packages/ui/src/Button.tsx
Affected: packages/ui, apps/web, apps/admin (dependents)
NOT run: packages/utils (unchanged, no downstream path)- Git base — compare PR branch to
origin/main - Turborepo —
--filter=...[origin/main]= changed + downstream dependents - Nx —
nx affected -t build --base=origin/main --head=HEAD - Full run on main — affected on PRs; complete graph on main merge
fetch-depth: 0 required — shallow checkout cannot diff against main. Lockfile change hits globalDependencies — full rebuild intentional.
Draw blast radius: changed package → direct dependents → apps importing them.
Interview: Affected only on changed package is wrong — must include transitive dependents.
Changesets & Package Versioning
Definition
Internal packages consumed by other teams need semver, not “just use main.”
---
"@acme/ui": minor
"@acme/utils": patch
---
Add Button loading state. Fix debounce zero-delay edge case.Developer → pnpm changeset → commit .changeset/*.md
Merge to main → bot opens "Version packages" PR
Merge version PR → CI runs changeset publishChangesets collects bump intent per package. changeset version bumps versions and changelogs. changeset publish publishes where version ≠ registry.
Apps using workspace:* update on version PR. External consumers get semver releases. Apps are usually private: true — deploy as artifacts, not npm publish.
Don’t say “we publish every commit.” Explain changeset → version PR → publish.
Interview: Required changeset policy on library PRs prevents version drift across teams.
Senior Interview Summary
Monorepo wins
Atomic changes, shared tooling, cacheable DAG. Coordination cost scales with package count.
Task graph
^build= upstream first. Parallel where graph allows. Persistent tasks not cacheable.
Cache key
inputs + env + dep output hashes. List env vars explicitly or get wrong hits.
Affected CI
--filter=...[origin/main]+ dependents. Full graph on main merge.
Changesets
Intent file → version PR → publish. Semver propagates through dependents.
Best mental model
Workspaces → link packages locally
Turbo/Nx → orchestrate + cache task DAG
Changesets → semver for publishable libsPractical rules
Lockfile in globalDependencies → dep bumps invalidate all
Include outputs in turbo.json → or cache restores empty dist
Nightly full graph + PR affected → safety vs speed balanceWhat to say in interviews
Design CI for a design system + three apps: explain dependsOn: ["^build"], remote cache, affected filter, and Changesets publish flow. Mention when to split monorepo back to packages + federation.