Skip to content
Fallow home
All docs pages

Architecture boundaries

Keep your architecture intact: define which directories may import from which, and fallow reports every import that breaks the rule. Start from a preset or define your own zones.

Architecture boundaries catch a UI component that imports the database client, or one feature that imports another. You define zones and declare which zones may import from which. fallow dead-code then reports each import that breaks a rule.

fallow dead-code --boundary-violations

fallow dead-code includes boundary violations in its output by default. To show only boundary issues, pass --boundary-violations.

Quick start with presets

A built-in preset is the fastest way to add boundaries. Fallow has four presets for common architecture patterns:

{
  "boundaries": {
    "preset": "bulletproof"
  }
}

To see the zones and rules that the preset creates, run fallow list --boundaries:

Boundaries: 6 zones, 6 rules, 1 logical group

Zones:
  app                  3 files  src/app/**
  features/auth        7 files  src/features/auth/**
  features/billing     5 files  src/features/billing/**
  features             0 files  src/features/**
  shared               8 files  src/components/**, src/hooks/**, src/lib/**, src/utils/**, src/utilities/**, src/providers/**, src/shared/**, src/types/**, src/styles/**, src/i18n/**
  server               4 files  src/server/**

Rules:
  features/auth        → shared, server
  features/billing     → shared, server
  app                  → features/auth, features/billing, features, shared, server
  features             → shared, server, features/auth, features/billing
  server               → shared
  shared               (isolated, no imports allowed)

Logical groups:
  features             12 files (2 children + 0 fallback)  autoDiscover: src/features
    children: features/auth, features/billing

The parent features zone holds only the top-level files in src/features/. Here it has none.

Presets

A common React and Next.js pattern. Feature modules cannot import each other. Shared utilities and server infrastructure are the base layers.

4 logical zones: app, features, shared, server. The features zone auto-discovers its immediate child directories, and each child becomes an isolated features/<name> zone.

Top-level files inside src/features/ (barrels, shared types) fall back to the parent features zone. The parent rule allows the discovered children. A barrel can therefore re-export features without false positives, and other top-level files still obey the features rule.

app              → every feature, shared, server
features/<name>  → shared, server
server           → shared
shared           (isolated)

The shared zone covers these directories: components, hooks, lib, utils, utilities, providers, shared, types, styles, i18n.

This preset matches the architecture from Bulletproof React, the most common pattern in React and Next.js projects.

Source root detection

Preset zone patterns use {rootDir}/{zone}/**. Fallow reads the source root from tsconfig.json:

{
  "compilerOptions": {
    "rootDir": "./lib"  // zones become lib/app/**, lib/features/**, etc.
  }
}

If tsconfig.json has no rootDir, fallow uses src.

Custom zones and rules

For full control, define the zones and rules yourself:

{
  "boundaries": {
    "zones": [
      { "name": "ui", "patterns": ["src/components/**", "src/pages/**"] },
      { "name": "data", "patterns": ["src/db/**", "src/api/**"] },
      { "name": "shared", "patterns": ["src/lib/**", "src/utils/**"] }
    ],
    "rules": [
      { "from": "ui", "allow": ["shared"] },
      { "from": "data", "allow": ["shared"] },
      { "from": "shared", "allow": [] }
    ]
  }
}

How zones work

  • A file belongs to the first zone whose pattern matches (first match wins).
  • A file that matches no zone is unrestricted. It can import from any zone, and any zone can import it.
  • Self-imports are always allowed. Files in the same zone can import each other.
  • A zone with no rule entry is unrestricted. It can import from any zone.
  • A zone with a rule and an empty allow list is isolated. It cannot import from other zones.
  • An import that does not run its target is not checked. require.resolve('./x') gives only a path, and a webpack asset loader request (raw-loader!./x.js) gives the text, the bytes or a URL of the file. Both keep the file in use, but no code crosses the boundary.
  • Boundary checks apply to every analyzed file, also a file that no entry point reaches. A script that only make, mise, or a CI step runs gets the same boundary findings as a reachable file. Such a file can also keep its unused-files finding.

Re-export chains

A named or default import through a barrel file is judged against the zone of the module that declares the symbol. For example, src/shared/index.ts re-exports a symbol from src/core/. An import of that symbol from ui is then judged as an import from core, not from shared.

  • to_path and to_zone name the origin module. The optional via_path field names the barrel that the file imports directly.
  • Fallow reports one finding per importer and origin module. A direct import of the origin module wins over an import through a barrel.
  • When a re-export in the barrel itself breaks a rule of the barrel zone, only the barrel gets a finding.
  • Namespace imports (import * as x) and side-effect imports (import "./x") are judged by the direct target, not by the origin modules.
  • Baseline keys stay from_path->to_path.

Auto-discovered feature zones

With autoDiscover, one logical zone creates one concrete zone per child directory. Use it to keep src/features/auth and src/features/billing apart without a zone and a rule for every feature.

{
  "boundaries": {
    "zones": [
      { "name": "app", "patterns": ["src/app/**"] },
      { "name": "features", "patterns": ["src/features/**"], "autoDiscover": ["src/features"] },
      { "name": "shared", "patterns": ["src/shared/**"] }
    ],
    "rules": [
      { "from": "app", "allow": ["features", "shared"] },
      { "from": "features", "allow": ["shared"] }
    ]
  }
}

If src/features/auth and src/features/billing exist, fallow expands this config to features/auth and features/billing. A rule for the logical features parent applies to every discovered feature. An explicit child rule, such as from: "features/auth", overrides the generated parent rule.

When the zone also has patterns, top-level files inside the auto-discover directory fall back to the parent zone. The parent rule allows the discovered children. For the full semantics, see Auto-discovered zones.

Subtree scope (root)

In a monorepo, each package usually has the same directory layout (packages/app/src/, packages/core/src/, ...). With patterns from the project root, you need one zone for each combination of layer and package:

// Without `root`: one zone per (layer, package) pair.
{ "name": "ui-app",      "patterns": ["packages/app/src/**"] },
{ "name": "domain-core", "patterns": ["packages/core/src/**"] }

Set root on a zone to limit its patterns to a subtree. Fallow checks that the file path starts with the root prefix. It removes that prefix and matches the patterns against the rest of the path. A file outside the subtree never matches the zone.

{
  "boundaries": {
    "zones": [
      { "name": "ui",     "patterns": ["src/**"], "root": "packages/app/" },
      { "name": "domain", "patterns": ["src/**"], "root": "packages/core/" }
    ],
    "rules": [
      { "from": "ui", "allow": [] }
    ]
  }
}

Here, packages/app/src/login.tsx is in ui and packages/core/src/order.ts is in domain. To add packages/billing/ later, add one zone entry with the same patterns: ["src/**"] and a different root.

Fallow normalizes trailing slashes and a leading ./, so "packages/app", "packages/app/", and "./packages/app/" are equivalent. It converts backslashes to forward slashes.

Do NOT repeat the root prefix in the patterns. Patterns are already relative to the root. When fallow resolves the config, it rejects root: "packages/app/" with patterns: ["packages/app/src/**"] and reports FALLOW-BOUNDARY-ROOT-REDUNDANT-PREFIX. Remove the root prefix from the pattern (patterns: ["src/**"]).

Overriding preset zones

You can start from a preset and change some zones or rules. A zone with the same name replaces the preset zone. A rule with the same from replaces the preset rule. For examples, see Merging presets with custom config.

Suppressing violations

To suppress one finding, add an inline comment:

// fallow-ignore-next-line boundary-violation
import { db } from '../data/client';

To suppress all boundary violations in a file:

// fallow-ignore-file boundary-violation

To turn off boundary checks, set the rule severity to off:

{
  "rules": {
    "boundary-violation": "off"
  }
}

The boundary-violation token and rule cover all boundary findings:

Fallow also accepts boundary-call-violation and boundary-call-violations as aliases. Each of the three tokens suppresses every boundary finding on that line or in that file.

To add boundaries to an existing codebase, start with "warn" severity. Fix the violations step by step, then switch to "error" when no violations remain.

Output formats

All output formats include boundary violations:

── Structure ─────────────────────────────────────

● Boundary violations (2)
  src/components/Button.tsx:1 → src/server/db/client.ts (shared → server)
  src/features/auth/login.ts:3 → src/features/billing/api.ts (features/auth → features/billing)
  Imports that cross defined architecture zone boundaries
  https://docs.fallow.tools/explanations/dead-code#boundary-violations

✗ 2 violations (0.05s)

See also