skip to content

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%

answer

  1. compile-time hard stop
  2. Bazel visibility / Nx tags
  3. human judgment vs mechanical check
  4. catches unreviewed/generated code too
  5. two layers, two failure modes

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.

solid answer

~40 s

Build-time boundary enforcement (Bazel `visibility` attributes, Nx's `enforce-module-boundaries` lint rule driven by project tags, Java's module-info) fails the build or lint step the moment code imports something it isn't allowed to depend on — a mechanical, compiler/CI-level stop evaluated on every commit, including ones a human review might rubber-stamp. CODEOWNERS only gates who must approve; it doesn't stop an import from compiling if a reviewer approves it, correctly or by mistake. Teams want both because they catch different failure modes: build-time rules catch accidental boundary violations cheaply and consistently, while human review catches judgment calls no linter can encode — is this dependency architecturally sound, should this API even be public. The machine handles the mechanical cases so reviewers focus on ones needing judgment.

go deeper

for a junior

Knows some monorepos have a lint or build step that blocks disallowed imports, separate from PR review.

for a middle

Can name at least one concrete mechanism, like Nx boundaries or Bazel visibility, and explain it blocks the build, not just the review.

for a senior

Designs the two-layer system deliberately — chooses what's enforced mechanically vs. left to review, and debugs boundary-violation build failures.

for a principal

Sets org policy for when to invest in build-time enforcement, weighing the cost of tagging and config against relying on review discipline, and plans migration as the module graph grows.

## Where a review gate stops Review-based ownership gates like CODEOWNERS operate at one specific point in a change's lifecycle: the moment a human reviewer decides whether to click approve. Everything upstream and downstream of that moment is untouched — the code still compiles, tests still run, and the change can still merge if a reviewer, correctly or by mistake, approves it, even if it violates an architectural rule nobody explicitly checked. **Build-time (or lint-time) boundary enforcement** closes that gap by encoding the rule itself as a mechanical check that runs in CI on every single change, independent of who reviewed it or whether a human reviewed it thoughtfully at all. ## How the ecosystems implement it Concretely, several ecosystems implement this. - **Bazel** — Google's build system, also used by projects like TensorFlow — has a `visibility` attribute on every build target that whitelists which other targets or packages are allowed to depend on it; adding a `deps` edge to a target whose visibility doesn't include your package fails the build with an explicit visibility error before any tests even run. - **Nx**, a popular tool for JavaScript/TypeScript monorepos, does something similar at the lint layer: every project gets one or more tags (e.g. `scope:payments`, `type:feature`, `type:util`), and an ESLint rule (`enforce-module-boundaries`) is configured with an explicit allow-list of which tag combinations may depend on which — importing across a disallowed tag pair fails lint, which typically fails CI. - **Java's module system** (`module-info.java`, since Java 9) enforces a related idea at the language/JVM level via `exports`/`requires` declarations. The common thread: the rule is declared once, centrally, and then mechanically enforced on every commit, not re-decided by a human each time. ## Why both layers coexist The reason both layers coexist rather than one replacing the other is that they catch fundamentally different classes of problems. Build-time enforcement is good at exactly the things that are cheap to express as a rule and expensive to catch by eyeballing a diff: did this PR add a dependency edge that shouldn't exist. It runs uniformly on every change, including changes a reviewer might rubber-stamp, changes generated by tooling, or changes touching dozens of files where a stray import is easy to miss visually. What it cannot do is judge anything requiring intent or design quality: - whether a new public function is well-named; - whether an allowed dependency is used in an architecturally sound way; - whether a shared utility's semantics changed in a way that will silently break a consumer even though the import graph is unchanged; - whether the whole approach to a feature is right. Those are exactly the judgment calls CODEOWNERS-gated human review exists for. ## What each layer costs Both layers have a trade-off. | Layer | Its price | |---|---| | Build-tool side | Upfront and ongoing configuration cost: someone has to define the tag taxonomy or visibility lists and keep it current as new projects are created and boundaries change, and an overly strict or stale ruleset produces false-positive build failures that block legitimate work, eroding trust in the mechanism the same way an overly slow CODEOWNERS gate does. | | Review side | Throughput and consistency — humans are slower and less consistent than a linter, but catch things no linter can express. | ## When only one layer exists A concrete failure mode when only one layer exists: - A team **relying solely on CODEOWNERS review** for boundary protection will eventually merge a disallowed cross-module import via a large, hurried PR near a deadline where the reviewer's attention was on feature logic, not the import list — nothing mechanically stops it. - Conversely, a team **relying solely on build-time visibility rules** with no review requirement can merge an architecturally sanctioned but functionally broken or badly designed change, because the linter has nothing to say about correctness or judgment — it only knows the dependency edge is technically permitted. Mature monorepos — Google's, and increasingly Nx- or Bazel-based industry monorepos — run both simultaneously precisely because each backstops the other's blind spot.

  • Could a team rely on build-tool visibility rules alone and drop CODEOWNERS?
    They could for mechanical dependency correctness, but they'd lose human review on non-import concerns — naming, design soundness, test quality, business-logic correctness — none of which visibility rules check at all.
  • What happens if a reviewer approves a PR that adds a disallowed import?
    The build/lint step still fails independently, so CI blocks the merge regardless of human approval — that's the point of layering a mechanical gate underneath the review gate rather than trusting review alone.
  • How do Nx module-boundary rules typically decide what's allowed?
    Each project gets tags such as scope:payments or type:feature, and lint config declares which tag-to-tag dependency edges are permitted, so importing across a disallowed tag pair fails lint before code review even starts.

CODEOWNERS is a security guard asking for ID before letting someone into a wing; build-tool visibility is a locked door that physically won't open without the right badge, guard or no guard.

saying these in an interview costs you the question

  • Treats CODEOWNERS and build-tool visibility as interchangeable or redundant
  • Believes code review alone reliably prevents architectural boundary violations
  • Doesn't know these tools operate at different stages — review-time vs. build-time
  • Assumes visibility enforcement requires manual per-file configuration with no tagging or grouping abstraction

context