skip to content

Monorepo & Codebase Organization

Keeping many projects in one versioned repository: shared tooling, a single history, atomic cross-project changes, and the ownership and scaling problems that come with a large repo. Expect to be asked to compare it with separate repos rather than to advocate for one.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

In a monorepo build system, what does it mean for a build task to get a 'cache hit', and why does that make rebuilds faster?

level: juniorimportance: must knowfreq 70%

answer

  1. pure function analogy
  2. content-addressed hash
  3. cache key = inputs hash
  4. hit vs miss
  5. stale output risk

basics

~20 s

A cache hit means the build system already ran this exact task before with the exact same inputs, so it reuses the saved output instead of redoing the work — like reheating leftovers instead of cooking again.

solid answer

~30 s

Build systems like Bazel, Nx, or Turborepo track every task (compile, test, lint) as a function of its inputs — source files, dependencies, config, environment. Before running a task, they hash those inputs into a cache key and check if an output for that exact key already exists locally or remotely. If yes, that's a cache hit: the system copies the stored output back instead of re-executing the task, often reducing a step from minutes to milliseconds. This lets a monorepo with thousands of packages rebuild only what actually changed instead of everything on every CI run or local build.

go deeper

for a junior

Should be able to explain in plain language that unchanged work is skipped and reused, using an analogy; doesn't need to know hashing details.

for a middle

Should know that keys are computed from file content hashes, not timestamps, and name a couple of real inputs (deps, source files).

for a senior

Should be able to explain how an under-specified cache key produces stale results and how they'd debug/fix it in a real pipeline.

for a principal

Should connect caching correctness to trust boundaries — e.g. why remote/shared caches need integrity guarantees beyond what a single dev's local cache needs.

## A build task as a pure function Every build task—compiling a package, running a linter, executing a test suite—can be thought of as a **pure function**: given the same inputs, it always produces the same output. A build system that wants to avoid redundant work exploits this by first computing a **cache key** for a task before running it, then checking whether a cached result already exists for that exact key. - **Hit** — if a matching result is found, that's a cache hit: the system fetches the previously stored output (compiled artifacts, test logs, exit code) and hands it back to whoever asked, without ever spawning the compiler or test runner. - **Miss** — if no match exists, that's a cache miss, and the task actually executes; its output is then stored under that key so future requests for the same inputs can hit the cache. ## What goes into the key The inputs that go into the key typically include: - the content of every source file the task reads; - the versions and content hashes of its dependencies; - the command-line flags and environment variables that affect its behavior; - the toolchain version itself (compiler binary, linter version). Because the key is derived from file content (usually via a hash like `SHA-256`) rather than file modification timestamps, this technique is called **content-addressed caching**—two files with identical bytes produce the same hash regardless of when they were last touched, which makes the cache immune to things like a `git clone` resetting timestamps or a CI checkout re-writing every file's mtime. ## Why it matters at monorepo scale Why this matters becomes obvious at monorepo scale. A repository with hundreds of packages might have a full build/test/lint pipeline that takes 30–60 minutes from a cold start. Most day-to-day changes touch only a handful of those packages. Without caching, every CI run and even every local build command re-executes every task for every package, because the build tool has no way to know that most of the graph is unchanged. With content-addressed caching, only the tasks whose actual inputs changed miss the cache; everything downstream that transitively depends on unchanged inputs also gets a cache hit, because its own inputs (which include the hashes of its upstream dependencies' outputs) are unchanged too. The practical effect is that a single-line change to one leaf package can produce a build that finishes in seconds instead of tens of minutes, because only that package and its direct consumers actually re-run. ## The trade-off The trade-off is bookkeeping overhead and correctness risk. Computing hashes for every input of every task costs CPU time and I/O, though this is normally far cheaper than re-running the task itself. The bigger risk is **under-specification**: if the cache key omits an input that actually affects the output — a stray environment variable, an implicit read of a config file not declared as a dependency, a non-deterministic code generator — the build system will confidently return a cache hit for a task whose real behavior has changed. This produces a 'stale' or 'wrong' result that looks identical to a correct one until someone notices the shipped artifact doesn't match the source. Teams debug this by identifying the untracked input and adding it to the task's declared inputs, or by making the task's declared inputs stricter (sandboxing filesystem/network access) so nothing can sneak in unnoticed. ## Failure modes in production In production this shows up in a few recognizable ways. 1. **First**, 'why didn't my change take effect' bugs, where a developer changes a file that (unknown to the build graph) is read by a task but not declared as an input, so the task keeps serving a stale cached build. 2. **Second**, flaky CI where a task is legitimately non-deterministic (e.g. embeds a timestamp or random UUID in its output) and so never gets a stable cache key, defeating caching for that node and everything downstream of it. 3. **Third**, security and trust concerns for shared/remote caches, where a malicious or buggy CI job could poison the shared cache with an incorrect artifact that then gets served to every other developer and pipeline that computes the same key. ## Where it shows up A concrete example: `Bazel` popularized this model for large monorepos at Google, using strict sandboxing to guarantee hermeticity so cache keys can be trusted; `Nx` and `Turborepo` bring a lighter-weight version of the same idea to JS/TS monorepos, hashing each project's source files plus its dependency graph to decide whether to replay a cached build versus actually invoking `webpack`, `tsc`, or `jest`. In all three, the fundamental unit of reasoning is the same: a task node in the dependency graph, a hash of everything that could affect its output, and a lookup against a store of previous results keyed by that hash.

  • What happens if two different tasks happen to produce the same cache key by coincidence?
    In practice this is astronomically unlikely with a strong hash function like SHA-256, since the key space is enormous and hash collisions would require a targeted attack rather than accident. Build systems generally trust the hash's collision resistance rather than defending against it explicitly. The much more common and real risk is not hash collision but an under-specified key — a task with a missing input feeding a wrong hit.
  • Does a cache hit skip the task's side effects too, like writing to a database during a test?
    Yes, and that is precisely why cacheable tasks must be side-effect-free or hermetic — a cache hit only replays previously captured outputs and logs, it never re-executes the task's code. If a 'test' task mutates external state as a side effect, caching it silently skips that mutation on a hit, which is a correctness bug in the task definition, not in the cache.
  • How does a build tool decide a task is a 'leaf' with no cacheable output, like a deploy step?
    Deploy or publish steps are usually marked non-cacheable in the task graph configuration precisely because they have external side effects (pushing to a registry) that shouldn't be skipped just because the same code was built before. Build tools like Nx and Turborepo let you flag which tasks participate in caching per task type.

Like a photocopier that first checks if it already has an identical page on file — if the page's contents match exactly, it hands you the existing copy instead of running the original through the machine again.

saying these in an interview costs you the question

  • Believes caching just means 'don't rebuild if the file's timestamp is old'
  • Can't explain what could cause a cache hit to serve stale/wrong output
  • Thinks caching only applies to the exact file that changed, not its downstream consumers
  • No mention of inputs beyond source code (env vars, deps, toolchain version)

context

open as a page

In a monorepo containing hundreds of independently deployable projects, why would a CI pipeline choose to only build and test the projects 'affected' by a given code change, rather than running the full build and test suite on every commit?

level: juniorimportance: must knowfreq 75%

basics

~20 s

Because rebuilding and testing everything every time gets too slow as the codebase grows. Affected detection figures out which projects a change could actually break and only runs those, so a small change gets fast feedback instead of a multi-hour full run.

open as a page

In a shared monorepo hosted on GitHub or GitLab, what does a CODEOWNERS file do, and how does it change what happens when someone opens a pull request?

level: juniorimportance: must knowfreq 65%

basics

~10 s

A CODEOWNERS file maps folders/files to people or teams. When a PR touches those files, the platform automatically asks the listed owners to review, and can require their approval before merge.

open as a page

In a monorepo containing many internal projects, what does it mean to have a single source of truth for a shared dependency's version, and why do teams enforce it instead of letting each project pin its own version independently?

level: juniorimportance: must knowfreq 65%

basics

~10 s

One place in the repo says "we use version X of this library" for everyone, so all teams building together use the same code and can't accidentally clash with incompatible versions.

open as a page

In a monorepo containing dozens of separate packages, why do teams adopt a dedicated build orchestrator (like Nx or Turborepo) instead of just writing a shell script that runs `build` and `test` in every package folder?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A shell script rebuilds and tests every package every run, which gets slow fast. A monorepo orchestrator tracks changes and dependencies between packages, so it only reruns what's actually affected and reuses cached results for the rest.

open as a page

What is the core difference between a monorepo and a polyrepo, and what's the main trade-off between them when it comes to sharing code and finding things across projects?

level: juniorimportance: must knowfreq 75%

basics

~20 s

A monorepo puts all your projects in one repository; a polyrepo splits them into many separate repositories. Monorepo: easy to find and share code, but everything lives together. Polyrepo: each project is independent, but sharing code across them takes more setup.

open as a page

Walk through what actually goes into computing a task's cache key in a monorepo build system, and why leaving something out is dangerous.

level: middleimportance: must knowfreq 75%

basics

~20 s

The cache key is a fingerprint made by hashing everything that could change the task's output — its source files, the versions of things it depends on, and its settings. If you forget to include something that matters, the cache can hand back a wrong, outdated answer without anyone noticing.

open as a page

Concretely, how does a build tool determine the 'affected' set of projects for a given code change — walk through the steps from a git diff to a final list of projects to build and test?

level: middleimportance: must knowfreq 70%

basics

~20 s

The tool lists which files changed, figures out which project each file belongs to, then looks at a map of 'who depends on whom' to find every other project that uses those changed projects, directly or through a chain. All of those become the affected set.

open as a page

Beyond CODEOWNERS-driven review, how do tools like Bazel visibility rules or Nx's module-boundary lint rule enforce project boundaries at build time, and why would a team want both that and human review?

level: middleimportance: must knowfreq 50%

basics

~20 s

Build tools can hard-block code from even compiling if it imports something outside its allowed visibility, unlike CODEOWNERS which only asks a human to review. Teams want both: humans catch design issues, the build catches accidental violations instantly on every change.

open as a page

A monorepo requires code-owner approval on every PR that touches a shared `libs/common-utils` package used by 30 teams. What problem does this create, and what are two ways to fix it without removing the review requirement entirely?

level: middleimportance: must knowfreq 55%

basics

~20 s

Every small change to a widely-used shared folder needs sign-off from a small owning team, so that team becomes a bottleneck. Fix it by splitting ownership into smaller pieces, or letting a wider team share the review load instead of one narrow gate covering everything.

open as a page

A team refactors a shared internal library's public function signature and needs to update 40 internal callers across a dozen projects at the same time. How does doing this inside a monorepo, in one atomic commit, differ from doing the equivalent in a multi-repo (polyrepo) setup where the library is published as a separately versioned package?

level: middleimportance: must knowfreq 70%

basics

~20 s

In a monorepo you change the library and all 40 callers in one commit that either fully passes or fully fails together. In separate repos, you'd publish a new library version first, then update each caller's dependency one at a time, with a gap where they're out of sync.

open as a page

How does remote build caching in tools like Bazel or Nx decide whether it can reuse a previous task's result instead of re-executing it, and what property does the task need to have for that reuse to be safe?

level: middleimportance: must knowfreq 75%

basics

~20 s

The tool fingerprints everything that could affect a task's output — its source files, commands, and dependencies — into one hash. If a previous run already produced and uploaded a result for that exact hash, it downloads that instead of rerunning. This only works if the same inputs always produce the same output.

open as a page

As a monorepo grows to hundreds of contributors and thousands of packages, what specific tooling investments are needed to keep builds, tests, and version control usable -- and what breaks if those investments aren't made?

level: middleimportance: must knowfreq 60%

basics

~20 s

Big monorepos need smart tools that only build/test the parts that actually changed, plus tricks so git doesn't have to download or scan the whole giant history. Without those tools, everything gets painfully slow as the repo grows.

open as a page

What does it mean for a build task to be 'hermetic', and what specifically breaks when a task isn't?

level: seniorimportance: must knowfreq 65%

basics

~20 s

A hermetic task only uses the inputs it's told about — no hidden reads from the network, system clock, or random files — so running it twice with the same inputs always gives the same result. If a task secretly depends on something outside that list, caching it becomes unsafe.

open as a page

Once the affected set of projects is known, how do build tools schedule the resulting build/test/lint tasks — what determines which tasks run in parallel versus which must wait, and how does this scale across multiple CI machines?

level: seniorimportance: must knowfreq 65%

basics

~20 s

Tasks form a chain based on which project needs which other project built first. Tasks with no unfinished dependency can run at the same time; tasks that depend on another task's output have to wait for it. Big pipelines split this work across several machines to go faster.

open as a page

When carving ownership boundaries in a monorepo, why do teams try to align project/module ownership tags with the org's team structure, and what goes wrong when that alignment drifts, per Conway's Law?

level: seniorimportance: must knowfreq 60%

basics

~10 s

Conway's Law says a system's architecture tends to mirror the communication structure of the org that built it. If module ownership doesn't match team structure, changes constantly need cross-team coordination, slowing everyone down.

open as a page

A monorepo enforces a single version of a widely-used internal logging library across 300 consuming projects. The library's maintainers want to ship a breaking change to its initialization API. What does a safe 'lockstep upgrade' process for this look like in practice, and what makes it different from just bumping the version and letting the build fail until people fix it?

level: seniorimportance: must knowfreq 60%

basics

~20 s

A safe lockstep upgrade updates the library AND every one of the 300 callers together in a controlled, often automated way (codemods, staged batches), instead of just breaking everyone's build at once and hoping they fix it themselves.

open as a page

When would a team choose Bazel over a lighter workspace-orchestration tool like Turborepo or Nx for their monorepo, and what does that choice cost them operationally?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Bazel is worth it when you need rock-solid reproducible builds across many languages and huge scale, but it takes real investment to set up and maintain. Turborepo and Nx are much easier to adopt for a JS-centric repo but give weaker guarantees and trust the underlying tools more.

open as a page

Conway's law says an organization's software structure tends to mirror its communication structure. How does that principle interact with the choice of monorepo vs polyrepo, and what does it mean in practice for how you'd decide to split or merge repositories as an org's team structure changes?

level: principalimportance: must knowfreq 40%

basics

~20 s

Conway's law says your code ends up organized like your teams are organized. Repo boundaries don't automatically fix bad team boundaries -- if teams don't talk or don't own things clearly, splitting or merging repos alone won't solve that; repo structure should follow real ownership lines, and should change when the team structure changes.

open as a page

What's the practical difference between a local build cache and a remote/distributed build cache in a monorepo, and what does a team give up by adopting the remote one?

level: middleimportance: should knowfreq 60%

basics

~20 s

A local cache only remembers work done on your own machine; a remote cache is shared, so if a teammate or CI already built something, you can download that result instead of building it yourself. The trade-off is speed and sharing versus needing a network connection and trusting what others uploaded.

open as a page

Library A and library B both live in the same monorepo and both depend on library C, but A was written against C's old API and B was written against C's newer API. What is this shape of conflict usually called, and why does building a project that depends on both A and B expose it, even though A and B individually build fine on their own?

level: middleimportance: should knowfreq 55%

basics

~20 s

It's called a diamond dependency conflict — A and B each want a different version/shape of C, and that's invisible until something needs both A and B at once, which forces C to be one single thing.

open as a page

A CI pipeline for a 200-package monorepo needs to decide which packages to test on a pull request that touches 3 files. Walk through how a monorepo orchestrator computes that 'affected' set, and what breaks if the dependency graph it relies on is incomplete.

level: middleimportance: should knowfreq 65%

basics

~20 s

It looks at which files changed, maps them to their owning packages, then walks the dependency graph outward to find every package that depends on those, directly or indirectly. If the graph is missing an edge, an affected package can be skipped and ship broken without CI catching it.

open as a page

How does working in a monorepo change the mechanics of making a breaking change to a widely-used shared library, compared to making that same change in a polyrepo where the library is a separately versioned package?

level: middleimportance: should knowfreq 55%

basics

~20 s

In a monorepo, you can update the shared code AND every place that uses it in one single commit, so nothing breaks. In a polyrepo, you publish a new version of the library, and every other team has to separately update to it later, so there can be a gap where things are out of sync.

open as a page

How does an in-process incremental compiler (like TypeScript's --incremental mode or a language server's recompilation) differ from build-system-level task caching, and where can each one still get correctness wrong?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A whole-task cache reuses a finished build's entire output if nothing changed; an incremental compiler stays running and only re-processes the specific files affected by an edit, keeping compiled state in memory or a save-file. Both skip redundant work, but the compiler's fine-grained version can go wrong if its internal dependency tracking misses a subtle edge case, like a change to a type that isn't textually referenced.

open as a page

What are the most common ways affected-detection-based CI goes wrong in production, and how do these failures typically show up — as a build error, or as something worse?

level: seniorimportance: should knowfreq 55%

basics

~20 s

The biggest danger is the tool missing a real connection between two parts of the code, so it thinks a project is safe when it isn't. CI stays green, but the broken thing ships anyway and only gets noticed later, usually in production.

open as a page

A company running a large monorepo is considering locking down write access per-directory so only the owning team can push to their own service's folder, mirroring a multi-repo permission model. What are the costs of doing this, and when would you deliberately keep write access repo-wide instead?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Locking write access per folder stops accidental cross-team edits but also blocks the fast, whole-repo refactors that make monorepos useful. Many teams keep write access open repo-wide and use review requirements instead, reserving hard restrictions for a few very sensitive paths.

open as a page

A team enables shared remote caching in their monorepo build tool. Soon after, engineers start seeing CI report a task as passing via a cache hit, even though the same code fails when run fresh locally. What's the likely root cause of this class of bug, and how do you prevent it going forward?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Some hidden input the task depends on (like an environment variable, network response, or timestamp) isn't included in the cache key. So a run that should be treated as different from a previous one gets matched to it anyway, and the old, wrong result gets replayed instead of rerunning.

open as a page

A polyrepo gets natural access control for free -- GitHub/GitLab repo-level permissions restrict who can see or push to a given project. How do organizations enforce equivalent access control inside a single monorepo, and what are the limits of that approach?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Since everything is in one repo, you can't just lock a whole repo per team. Instead, tools restrict access by folder -- who can approve merges where -- using code-review rules, and for real secrets, keeping them in a separate system entirely.

open as a page

At what point does investing in sophisticated build caching (remote cache, hermetic sandboxing, fine-grained incrementality) stop paying for itself for a growing monorepo, and what would make you recommend against it?

level: principalimportance: should knowfreq 35%

basics

~20 s

Fancy caching systems cost real engineering time to build, run, and keep trustworthy. For a small codebase or a team that rebuilds rarely, that cost can be bigger than the minutes it saves, so the right call is sometimes to just let builds run slow rather than build a whole caching platform.

open as a page

How do you keep an affected-detection CI pipeline fast and reliable as a monorepo grows into the thousands of projects — what specifically starts to break, and what strategies address it?

level: principalimportance: should knowfreq 40%

basics

~20 s

As the codebase gets huge, even the 'figure out what's affected' step and the safety-net full runs get slow, and a few heavily-used shared projects become bottlenecks everyone waits on. You fix this by splitting work across many machines, caching aggressively, keeping the dependency map itself fast to compute, and giving special treatment to the most-depended-on projects.

open as a page

showing 1–30 of 33