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.
Classic N-tier architecture with four layers. Infrastructure may import from domain and from application, which is common with dependency injection.
4 zones: presentation, application, domain, infrastructure
presentation → application
application → domain
domain (isolated)
infrastructure → domain, application
Ports and adapters pattern. The domain has zero outward dependencies. Adapters implement port interfaces.
3 zones: adapters, ports, domain
adapters → ports
ports → domain
domain (isolated)
Feature-Sliced Design with strict downward imports. Each layer may import only from the layers below it.
6 zones: app, pages, widgets, features, entities, shared
app → pages, widgets, features, entities, shared
pages → widgets, features, entities, shared
widgets → features, entities, shared
features → entities, shared
entities → shared
shared (isolated)
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
allowlist 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 itsunused-filesfinding.
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_pathandto_zonename the origin module. The optionalvia_pathfield 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:
- import-direction violations
- coverage violations
- forbidden-call violations
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){
"boundary_violations": [
{
"from_path": "src/features/auth/login.ts",
"to_path": "src/features/billing/api.ts",
"from_zone": "features/auth",
"to_zone": "features/billing",
"import_specifier": "src/features/billing/api.ts",
"line": 3,
"col": 9
}
]
}The full JSON output also has schema_version, version, elapsed_ms, and the arrays for all issue types. Each violation also has an actions array with fix and suppression hints. This example shows only boundary_violations.
When the violation comes from a re-export chain, to_path is the origin module and the optional via_path field names the barrel. A direct import has no via_path. The human output shows the barrel as via <path> after the zones.
boundary-violation:src/features/auth/login.ts:3:src/features/auth/login.ts -> src/features/billing/api.ts (features/auth -> features/billing)