Skip to content
fearchitect
Architecture & Composition

Monorepos (Turborepo / Nx)

One git repo, many packages, shared tooling and task caching.

By Abas TurabliReviewed

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 angle

A 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.

turbo.json tasksjson
{
  "$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.

Turborepo runs tasks bottom-up: `packages/utils` builds first, then `packages/ui`, then both apps — in parallel where the graph allows.

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.

Further reading

Search fearchitect

Jump to a topic, mode, or action.