Introduction
Boundary is a static analysis tool that validates architectural boundaries in codebases following Domain-Driven Design (DDD) and Hexagonal Architecture patterns. It automatically detects architectural violations, scores adherence to architectural principles, and generates visual documentation of system boundaries and dependencies.
Why Boundary?
Architectural rules often live in wikis or team knowledge but aren’t enforced in code. Over time, boundaries erode: domain logic leaks into infrastructure, adapters skip port interfaces, and layers become tightly coupled. Manual code review catches some of these issues, but not at scale.
Boundary solves this by:
- Detecting violations automatically – Catch domain-to-infrastructure dependencies before they reach production
- Quantifying architectural health – Objective scores for layer isolation and dependency flow
- Generating documentation – Up-to-date architecture diagrams generated from code
- Integrating with CI/CD – Fail builds on critical violations
Supported Languages
Boundary uses tree-sitter for multi-language AST parsing:
- Go
- Rust
- TypeScript / TSX
- Java
How It Works
The analysis pipeline follows these steps:
- Parse – Build ASTs for each source file using tree-sitter
- Extract – Identify components (interfaces, structs, imports, dependencies)
- Classify – Assign components to architectural layers (Domain, Application, Infrastructure, Presentation)
- Build Graph – Construct a dependency graph with layer metadata using petgraph
- Analyze – Detect violations and calculate scores
- Report – Output results as text, JSON, Markdown, or diagrams
Architecture
boundary (CLI)
├── boundary-core -- Analyzer trait, graph types, scoring, violations
├── boundary-go -- Go language analyzer
├── boundary-rust -- Rust language analyzer
├── boundary-typescript -- TypeScript/TSX analyzer
├── boundary-java -- Java language analyzer
├── boundary-report -- Report generation (text, markdown, mermaid, DOT)
└── boundary-lsp -- LSP server for editor integration
Installation
Homebrew (macOS / Linux)
brew install rebelopsio/tap/boundary
This installs both boundary and boundary-lsp.
Pre-built Binaries
Download the latest release for your platform from GitHub Releases.
Both boundary and boundary-lsp are included in each release archive. Binaries are available for:
- macOS (Apple Silicon and Intel)
- Linux (x86_64)
- Windows (x86_64)
Install from Source
With a Rust toolchain installed (rustup.rs):
cargo install --git https://github.com/rebelopsio/boundary boundary boundary-lsp
Or clone and build locally:
git clone https://github.com/rebelopsio/boundary.git
cd boundary
cargo build --release
# Binaries are at target/release/boundary and target/release/boundary-lsp
Verify Installation
boundary --version
Quick Start
1. Initialize Configuration
In your project root, generate a .boundary.toml config file:
boundary init
This creates a .boundary.toml with sensible defaults for Go projects. Edit it to match your project structure.
2. Run Analysis
Analyze your codebase and see the full architecture report:
boundary analyze .
The output includes:
- Detected components grouped by architectural layer
- Violations with file paths and line numbers
- Architecture scores (0–100%) broken down by structural presence, layer isolation, dependency direction, and interface coverage
3. Check in CI
Use boundary check to get a pass/fail exit code suitable for CI pipelines:
boundary check . --fail-on error
Exit codes:
0— No violations at or above the failure threshold1— Violations found
4. Track Progress Over Time
Record a snapshot of the current architecture score and prevent regressions from being merged:
# Record a snapshot
boundary check . --track
# Fail if the score drops below the last recorded snapshot
boundary check . --no-regression
# Do both in one step (typical CI setup)
boundary check . --track --no-regression
Snapshots are stored in .boundary/history.ndjson relative to the project root. If no snapshot has been recorded yet, --no-regression is a no-op.
5. Generate Diagrams
Produce architecture diagrams in Mermaid or GraphViz DOT format:
# Mermaid layer diagram
boundary diagram .
# GraphViz DOT dependency graph
boundary diagram . --diagram-type dot-dependencies
6. Deep-Dive Forensics
Inspect a specific module for DDD pattern adherence:
boundary forensics path/to/module
This shows per-aggregate analysis, domain event detection, port/adapter mapping, and improvement suggestions.
Example Output
Boundary - Architecture Analysis
========================================
Overall Score: 85%
Structural Presence: 100%
Layer Isolation: 80%
Dependency Direction: 90%
Interface Coverage: 75%
Summary: 30 components, 12 dependencies
Metrics
----------------------------------------
Components by layer:
Application: 8
Domain: 12
Infrastructure: 6
Presentation: 4
Dependency depth: max=3, avg=1.2
Violations (2 found)
----------------------------------------
ERROR [domain -> infrastructure] internal/domain/user/repository.go
Domain layer must not depend on Infrastructure
Suggestion: Define a port interface in the domain layer and inject the implementation
WARN [missing port for PaymentAdapter] internal/infrastructure/payment/stripe.go
Adapter has no corresponding port interface in the domain or application layer
Suggestion: Add a port interface that this adapter implements
CHECK FAILED: 1 violation(s) at severity error or above
Configuration: .boundary.toml
Boundary is configured via a .boundary.toml file. Run boundary init to generate a starter config.
Config Discovery
Boundary searches for .boundary.toml starting from the analysis target directory and walking up parent directories (similar to how Git finds .git). The first config file found is used. If no config is found, built-in defaults are used.
This means you can place .boundary.toml at the repository root and analyze any subdirectory — the config will be discovered automatically.
Full Reference
[project]
languages = ["go"]
exclude_patterns = ["vendor/**", "**/*_test.go", "**/testdata/**"]
# services_pattern = "services/*" # For monorepo per-service analysis
[layers]
# Glob patterns to classify files into architectural layers.
domain = ["**/domain/**", "**/entity/**", "**/model/**"]
application = ["**/application/**", "**/usecase/**", "**/service/**"]
infrastructure = ["**/infrastructure/**", "**/adapter/**", "**/repository/**", "**/persistence/**"]
presentation = ["**/presentation/**", "**/handler/**", "**/api/**", "**/cmd/**"]
# Paths exempt from layer violation checks (cross-cutting concerns)
# cross_cutting = ["common/utils/**", "pkg/logger/**", "pkg/errors/**"]
# Global architecture mode: "ddd" (default), "active-record", or "service-oriented"
# architecture_mode = "ddd"
[scoring]
# Weights for score components (should sum to 1.0)
layer_isolation_weight = 0.4
dependency_direction_weight = 0.4
interface_coverage_weight = 0.2
[rules]
# Minimum severity to cause failure: "error", "warning", or "info"
fail_on = "error"
# min_score = 70.0 # Optional minimum architecture score
# detect_init_functions = true # Detect Go init() side effects
[rules.severities]
layer_boundary = "error"
circular_dependency = "error"
missing_port = "warning"
init_coupling = "warning"
Sections
[project]
| Key | Type | Default | Description |
|---|---|---|---|
languages | list | [] (auto-detect) | Languages to analyze. Options: go, rust, typescript, java |
exclude_patterns | list | ["vendor/**", "**/*_test.go", "**/testdata/**"] | Glob patterns for files to skip |
services_pattern | string | (none) | Glob for service directories in monorepos (e.g., "services/*") |
[layers]
Each layer accepts a list of glob patterns. Files matching a pattern are classified into that layer.
| Key | Default Patterns |
|---|---|
domain | **/domain/**, **/entity/**, **/model/** |
application | **/application/**, **/usecase/**, **/service/** |
infrastructure | **/infrastructure/**, **/adapter/**, **/repository/**, **/persistence/** |
presentation | **/presentation/**, **/handler/**, **/api/**, **/cmd/** |
Additional fields:
| Key | Type | Description |
|---|---|---|
cross_cutting | list | Paths exempt from layer violation checks (applies to both source files and import targets) |
architecture_mode | string | Global mode: "ddd", "active-record", or "service-oriented" |
[[layers.overrides]]
Per-module overrides for layer classification. The first matching scope wins.
[[layers.overrides]]
scope = "services/auth/**"
domain = ["services/auth/core/**"]
infrastructure = ["services/auth/server/**", "services/auth/adapters/**"]
# architecture_mode = "active-record" # Optional per-module mode
Omitted layers fall back to the global patterns.
[scoring]
| Key | Default | Description |
|---|---|---|
layer_isolation_weight | 0.4 | Weight for layer isolation score |
dependency_direction_weight | 0.4 | Weight for dependency direction score |
interface_coverage_weight | 0.2 | Weight for interface coverage score |
Weights should sum to 1.0.
[rules]
| Key | Type | Default | Description |
|---|---|---|---|
fail_on | string | "error" | Minimum severity to cause non-zero exit |
min_score | float | (none) | Optional minimum overall score |
detect_init_functions | bool | true | Detect Go init() side-effect coupling |
[rules.severities]
Override the default severity for built-in violation types. Both category names and rule IDs are accepted as keys. Rule IDs take precedence over category names.
Category Names
| Category Name | Default Severity | Description |
|---|---|---|
layer_boundary | error | Inner layer depends on outer layer |
circular_dependency | error | Circular dependency between components |
missing_port | warning | Adapter without a corresponding port interface |
constructor_concrete | warning | Constructor returns concrete type instead of port |
init_coupling | warning | Go init() function creates hidden coupling |
domain_infra_leak | error | Domain references infrastructure types |
Rule IDs
You can also use specific rule IDs (e.g., L001, PA001) for more granular control:
[rules.severities]
missing_port = "warning" # Category-wide default
PA001 = "info" # Override just missing-port-interface to info
See Rules & Rule IDs for the full rule catalog.
[[rules.ignore]]
Suppress specific rules for files matching glob patterns:
[[rules.ignore]]
rule = "PA001"
paths = ["infrastructure/**/*document.go"]
[[rules.ignore]]
rule = "L005"
paths = ["legacy/**"]
| Key | Type | Description |
|---|---|---|
rule | string | Rule ID to suppress (e.g., PA001, L001) |
paths | list | Glob patterns — violation is suppressed if the file matches any pattern |
Custom Rules
Define custom dependency rules:
[[rules.custom_rules]]
name = "no-http-in-domain"
from_pattern = "**/domain/**"
to_pattern = "**/net/http**"
action = "deny"
severity = "error"
message = "Domain layer must not import HTTP packages"
| Key | Description |
|---|---|
name | Rule identifier |
from_pattern | Glob for the source of the dependency |
to_pattern | Glob for the target of the dependency |
action | "deny" (only option currently) |
severity | "error", "warning", or "info" |
message | Custom violation message |
Layer Analysis
Boundary classifies source code components into four architectural layers and enforces dependency rules between them.
Architectural Layers
From innermost (most protected) to outermost:
| Layer | Purpose | Default Patterns |
|---|---|---|
| Domain | Core business logic, entities, value objects | **/domain/**, **/entity/**, **/model/** |
| Application | Use cases, application services, orchestration | **/application/**, **/usecase/**, **/service/** |
| Infrastructure | Database adapters, external APIs, persistence | **/infrastructure/**, **/adapter/**, **/repository/**, **/persistence/** |
| Presentation | HTTP handlers, CLI, API controllers | **/presentation/**, **/handler/**, **/api/**, **/cmd/** |
Dependency Rules
The core rule is that inner layers must not depend on outer layers:
Domain ← Application ← Infrastructure
← Presentation
Valid dependencies:
- Application can import from Domain
- Infrastructure can import from Domain and Application
- Presentation can import from Domain and Application
Violations:
- Domain importing from Infrastructure or Presentation
- Application importing from Infrastructure or Presentation
- Any circular dependency between layers
Scoring
Boundary calculates three sub-scores that combine into an overall architecture score (0–100):
| Score | Default Weight | What It Measures |
|---|---|---|
| Layer Conformance | 40% | How closely each package’s (A, I) values match its assigned layer’s expected region |
| Dependency Compliance | 40% | Fraction of cross-layer imports that flow in the correct direction |
| Interface Coverage | 20% | Balance between domain port interfaces and infrastructure adapters |
Interface Coverage
Interface coverage measures how well your infrastructure layer uses ports (interfaces) to decouple from the domain. Boundary counts:
- Ports: Exported interfaces in the Domain layer
- Adapters: Components in the Infrastructure layer with kind
Adapter,Repository, orService
The score is min(ports, adapters) / max(ports, adapters) * 100. If there are no infrastructure adapters, the dimension is undefined and omitted from output — it is never defaulted to 100.
Go-specific adapter detection
In Go, infrastructure adapters commonly use unexported concrete types paired with an exported constructor:
// unexported concrete type — boundary counts this as a real component
type mongoUserRepository struct { ... }
// exported constructor — the usual Go idiom
func NewMongoUserRepository() ports.UserRepository {
return &mongoUserRepository{}
}
Boundary includes unexported structs from the infrastructure layer in all component counts and interface coverage calculations.
Structs named *Handler or *Controller in the application or presentation layers are treated as orchestrators, not adapters, and are not counted toward interface coverage. Infrastructure-layer handlers (driving/primary adapters) are counted as infrastructure components.
Component Extraction
Boundary identifies these component types from source code:
- Interfaces / Traits – Port definitions
- Structs / Classes – Entities, value objects, adapters
- Imports – Dependency relationships between components
- Functions – Service methods, handlers
Automatic Filtering
Standard Library Imports
Standard library imports are automatically excluded from the dependency graph. For Go, any import path without a dot (e.g., fmt, encoding/json) is recognized as stdlib. This prevents stdlib packages from inflating the unclassified component count.
External Dependencies
Import targets that don’t correspond to any source file in the project (e.g., third-party libraries like github.com/stripe/stripe-go) are automatically treated as cross-cutting. They appear in the dependency graph but don’t trigger layer violations.
Cross-Cutting Concerns
Some packages (logging, error handling, utilities) don’t belong to any layer. Configure these as cross-cutting concerns to exclude them from violation checks:
[layers]
cross_cutting = ["common/utils/**", "pkg/logger/**", "pkg/errors/**"]
Cross-cutting patterns apply to both source files and import targets. Use ** glob patterns for best results:
cross_cutting = ["**/methods/**", "**/observability/**", "**/uptime/**"]
Cross-cutting components are still tracked in the dependency graph for visualization, but dependencies to/from them don’t count as violations.
Anemic Domain Model Detection
Boundary flags domain entities that have no business methods as potential anemic domain models. This check only applies to components in the domain layer — infrastructure DTOs and data transfer objects in other layers are not flagged.
Custom Layer Patterns
Override the default patterns in .boundary.toml to match your project structure:
[layers]
domain = ["**/core/**", "**/models/**"]
application = ["**/app/**", "**/usecases/**"]
infrastructure = ["**/infra/**", "**/db/**", "**/clients/**"]
presentation = ["**/web/**", "**/grpc/**"]
For monorepos with per-service structures, use layer overrides.
Architecture Modes
Not every codebase follows strict DDD patterns. Boundary supports multiple architecture modes to reduce false positives and match your project’s actual design.
Available Modes
ddd (default)
Strict Domain-Driven Design. Enforces full layer separation:
- Domain entities must not import infrastructure packages
- Adapters must have corresponding port interfaces
- All layer boundary violations are flagged
Best for: projects following hexagonal or clean architecture patterns.
active-record
Relaxed rules for Active Record patterns where domain entities contain persistence logic (e.g., .Save(), .Load() methods that call the database directly):
- Domain entities importing database drivers are not flagged
- Port/adapter coverage requirements are relaxed
- Layer isolation scoring adjusts expectations
Best for: CRUD-heavy services, Rails-style codebases, or modules where full DDD adds unnecessary complexity.
service-oriented
Designed for service-oriented architectures where the traditional layer model doesn’t apply:
- Looser coupling requirements between components
- Focus on service boundary enforcement rather than layer isolation
Best for: microservices with flat internal structure, legacy codebases being gradually improved.
Global Configuration
Set the architecture mode for the entire project:
[layers]
architecture_mode = "active-record"
Per-Module Overrides
Real codebases often use different patterns in different modules. Configure per-module modes with layer overrides:
# Complex domain logic gets strict DDD
[[layers.overrides]]
scope = "services/billing/**"
architecture_mode = "ddd"
domain = ["services/billing/core/**"]
infrastructure = ["services/billing/adapters/**"]
# Simple CRUD module uses Active Record
[[layers.overrides]]
scope = "services/notifications/**"
architecture_mode = "active-record"
Cross-module dependencies still enforce layer rules at module boundaries, regardless of each module’s internal mode.
Rules & Rule IDs
Every violation Boundary reports carries a rule ID — a short, stable identifier like
L001 or PA001. Rule IDs let you selectively suppress false positives, filter output, and
(in the future) configure severity per rule.
Rule Catalog
Layer Violations (L)
| ID | Name | Description | Severity |
|---|---|---|---|
| L001 | domain-depends-on-infrastructure | Domain layer imports directly from infrastructure | Error |
| L002 | domain-depends-on-application | Domain layer depends on application orchestration | Error |
| L003 | application-bypasses-ports | Application layer calls infrastructure without a port | Error |
| L004 | init-function-coupling | Init/main wiring function couples layers directly | Warning |
| L005 | domain-uses-infrastructure-type | Domain code references an infrastructure type | Error |
| L099 | layer-boundary-violation | Catch-all for other forbidden layer crossings | Error |
Dependency Violations (D)
| ID | Name | Description | Severity |
|---|---|---|---|
| D001 | circular-dependency | Circular dependency detected between components | Error |
Port/Adapter Violations (PA)
| ID | Name | Description | Severity |
|---|---|---|---|
| PA001 | missing-port-interface | Infrastructure adapter has no matching domain port | Warning |
| PA003 | constructor-returns-concrete-type | Constructor returns concrete type instead of port interface | Warning |
PA003: constructor-returns-concrete-type
Detects constructors in the infrastructure layer that return a concrete struct pointer instead of a port interface. This is a Dependency Inversion Principle violation — callers become coupled to the concrete implementation rather than depending on an abstraction.
Violation:
// infrastructure/mailgun/service.go
func NewMailGunService(apiKey string) *MailGunService {
return &MailGunService{apiKey: apiKey}
}
Fix: Return the port interface instead:
// infrastructure/mailgun/service.go
func NewMailGunService(apiKey string) ports.NotificationService {
return &MailGunService{apiKey: apiKey}
}
When PA003 fires, PA001 (missing-port-interface) is suppressed for the same adapter since PA003 provides more specific guidance.
Custom Rules (C-)
Custom rules defined in .boundary.toml receive IDs prefixed with C- followed by the rule
name. For example, a rule named no-logging-in-domain gets the ID C-no-logging-in-domain.
See Custom Rules for how to define them.
Configuration
Severity Overrides
Override the default severity for any rule using its rule ID or category name in
[rules.severities]:
[rules.severities]
# Category names (backward compatible)
layer_boundary = "error"
missing_port = "warning"
domain_infra_leak = "error"
# Rule IDs take precedence over category names
PA001 = "info"
L001 = "warning"
When both a rule ID and category name are configured, the rule ID wins. This lets you set a baseline per category and override individual rules.
Path-specific Ignores
Suppress specific rules for files matching glob patterns:
[[rules.ignore]]
rule = "PA001"
paths = ["infrastructure/**/*document.go"]
[[rules.ignore]]
rule = "L005"
paths = ["legacy/**"]
Unlike --ignore (which suppresses a rule globally), path-specific ignores only suppress
violations in files matching the glob patterns. This is useful when certain areas of the
codebase intentionally diverge from the architecture (e.g., legacy modules undergoing
migration).
Ignoring Rules
Use --ignore to suppress specific rules by ID. This is useful for false positives or rules
that don’t apply to your codebase.
# Ignore a single rule
boundary analyze . --ignore PA001
# Ignore multiple rules (comma-separated)
boundary analyze . --ignore PA001,L005
# Works with check too — ignored violations don't affect the exit code
boundary check . --ignore PA001
Ignored violations are removed before output formatting and before the check pass/fail
decision.
Output Format
Rule IDs appear in all output formats.
Text
L001 ERROR [domain-depends-on-infrastructure] domain/user.go:10
Domain component imports infrastructure package
Suggestion: Define a port interface in the domain layer
JSON
Each violation includes rule and rule_name fields:
{
"rule": "L001",
"rule_name": "domain-depends-on-infrastructure",
"kind": { "LayerBoundary": { "from_layer": "Domain", "to_layer": "Infrastructure" } },
"severity": "error",
"location": { "file": "domain/user.go", "line": 10, "column": 1 },
"message": "Domain component imports infrastructure package"
}
Filter by rule ID with jq:
# Show only L001 violations
boundary analyze . --format json | jq '.violations[] | select(.rule == "L001")'
# Count PA001 occurrences
boundary analyze . --format json | jq '[.violations[] | select(.rule == "PA001")] | length'
Markdown
| Rule | Severity | Name | Location | Message |
|------|----------|------|----------|---------|
| L001 | ERROR | domain-depends-on-infrastructure | domain/user.go:10 | ... |
Custom Violation Rules
Boundary’s built-in rules catch layer boundary violations, circular dependencies, and missing ports. Custom rules let you enforce additional architectural constraints specific to your project.
Defining a Custom Rule
Add one or more [[rules.custom_rules]] entries to .boundary.toml:
[[rules.custom_rules]]
name = "no-domain-external"
from_pattern = ".*domain.*"
to_pattern = ".*external.*"
action = "deny"
severity = "warning"
message = "Domain must not import external packages"
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier shown in violation output |
from_pattern | Yes | Regex matched against the source component’s path |
to_pattern | Yes | Regex matched against the import path of the dependency |
action | No | Only "deny" is supported (default: "deny") |
severity | No | "error", "warning", or "info" (default: "error") |
message | No | Custom violation message; a default is generated if omitted |
How Matching Works
from_pattern is matched against the source component’s component ID — the package path
plus the component name, e.g. internal/domain/user::<file>.
to_pattern is matched against the import path recorded in the dependency edge, e.g.
github.com/acme/app/external/payments.
Both patterns are full regular expressions (via the Rust regex crate). Use .* to match
any path segment.
Examples
Prevent domain from importing specific packages
[[rules.custom_rules]]
name = "no-http-in-domain"
from_pattern = ".*domain.*"
to_pattern = ".*/net/http$"
action = "deny"
severity = "error"
message = "Domain layer must not import net/http directly"
Warn when a deprecated package is imported anywhere
[[rules.custom_rules]]
name = "no-legacy-client"
from_pattern = ".*"
to_pattern = ".*/legacy/client.*"
action = "deny"
severity = "warning"
message = "legacy/client is deprecated — use clients/v2 instead"
Multiple rules fire independently
[[rules.custom_rules]]
name = "no-domain-db"
from_pattern = ".*domain.*"
to_pattern = ".*/database.*"
severity = "error"
message = "Domain must not import database packages directly"
[[rules.custom_rules]]
name = "no-domain-redis"
from_pattern = ".*domain.*"
to_pattern = ".*/redis.*"
severity = "warning"
message = "Domain must not import redis packages directly"
Each rule is evaluated independently. A single dependency edge can trigger multiple rules if it matches more than one pattern pair.
Violation Output
Custom rule violations appear in all output formats alongside built-in violations:
WARN [custom: no-domain-external] internal/domain/user/entity.go:4
Domain must not import external packages
Suggestion: This dependency is forbidden by custom rule 'no-domain-external'.
In JSON output, custom rule violations have kind.CustomRule.rule_name set to the rule’s
name field.
Severity and check Behaviour
Custom rule severity interacts with boundary check --fail-on the same way built-in
violations do:
# Warning-severity custom rules pass a check at the error threshold
boundary check . --fail-on error # exits 0 if only warnings present
# Lower the threshold to catch warnings too
boundary check . --fail-on warning # exits 1
Reports
Boundary produces reports in three formats: plain text (default), JSON, and Markdown.
boundary analyze . --format text # default — coloured terminal output
boundary analyze . --format json # machine-readable
boundary analyze . --format markdown # suitable for wikis and PR comments
Markdown Format
The Markdown report is designed to be pasted into GitHub PR descriptions, wikis, Confluence pages, or any Markdown renderer. Every section that has data is rendered; sections with no data are omitted.
boundary analyze . --format markdown
boundary analyze . --format markdown > architecture.md
Sections
Scores
Overall architecture score and each sub-dimension, rendered as a table.
## Scores
| Metric | Score |
|-------------------------|--------------|
| **Overall** | **78.0/100** |
| Structural Presence | 100.0/100 |
| Layer Conformance | 85.0/100 |
| Dependency Compliance | 72.0/100 |
| Interface Coverage | 60.0/100 |
Summary
Total component and dependency counts.
Metrics
Components by layer, components by kind, dependency depth, and classification coverage.
Package Metrics
Robert C. Martin’s package-level coupling metrics — Instability (I), Abstractness (A), and Distance from the main sequence (D) — for each package in the project.
## Package Metrics
| Package | A | I | D | Zone |
|----------------|------|------|------|-------------|
| domain | 0.50 | 0.00 | 0.50 | — |
| application | 0.00 | 1.00 | 0.00 | — |
| infrastructure | 0.00 | 1.00 | 0.00 | — |
| common | 0.00 | 0.00 | 1.00 | ⚠ Pain |
The Zone column is populated when a package is far from the main sequence (D > 0.5):
| Zone | Condition | Meaning |
|---|---|---|
| ⚠ Pain | A < 0.5 and I < 0.5 | Concrete and stable — rigid, hard to change |
| ⚠ Uselessness | A > 0.5 and I > 0.5 | Abstract and unstable — unused abstractions |
| — | otherwise | On or near the main sequence |
See scoring concepts for the full metric definitions.
Pattern Detection
The detected architectural pattern and confidence scores for all five patterns.
## Pattern Detection
Top Pattern: **ddd-hexagonal** (78% confidence)
| Pattern | Confidence |
|----------------|------------|
| ddd-hexagonal | 78% |
| service-layer | 35% |
| anemic-domain | 20% |
| flat-crud | 5% |
| active-record | 0% |
Confidence values are independent — they do not sum to 100%. A codebase in transition may show meaningful confidence for multiple patterns simultaneously.
Violations
All violations in a table, with rule ID, severity, rule name, location, and message.
| Rule | Severity | Name | Location | Message |
|------|----------|------|----------|---------|
| L001 | ERROR | domain-depends-on-infrastructure | domain/user.go:10 | Domain depends on infra |
| PA001 | WARN | missing-port-interface | infrastructure/repo.go:5 | No matching port |
See Rules & Rule IDs for the full rule catalog.
JSON Format
JSON output includes every field, suitable for programmatic processing, dashboards, or saving snapshots.
boundary analyze . --format json | jq '.score.overall'
boundary analyze . --format json | jq '.violations[] | select(.severity == "error")'
boundary analyze . --format json | jq '.package_metrics[] | select(.zone != null)'
Top-level fields in the JSON output:
| Field | Description |
|---|---|
score | Architecture score dimensions (omitted if pattern confidence < 0.5) |
violations | Array of all violations |
component_count | Total number of real components |
dependency_count | Total number of dependency edges |
files_analyzed | Number of source files analyzed |
metrics | Detailed metrics breakdown |
package_metrics | Array of per-package A/I/D metrics |
pattern_detection | Pattern confidence distribution |
Each violation object includes:
| Field | Description |
|---|---|
rule | Stable rule ID (e.g. L001, PA001, D001) |
rule_name | Human-readable rule name (e.g. domain-depends-on-infrastructure) |
kind | Violation kind with structured details |
severity | error, warning, or info |
location | File path, line, and column |
message | Human-readable description |
suggestion | Fix suggestion (when available) |
Filter violations by rule ID with jq:
boundary analyze . --format json | jq '.violations[] | select(.rule == "L001")'
Text Format
The default terminal output with colour highlighting. Designed for developer workflows and CI log readability.
boundary analyze . # coloured output
boundary analyze . --compact # single-line JSON, no colour (useful for piping)
Architecture Diagrams
Boundary can generate architecture diagrams in Mermaid and GraphViz DOT formats, showing how components are organized into layers and how they depend on each other.
boundary diagram <PATH> [--diagram-type <TYPE>]
Diagram Types
| Type | Format | Description |
|---|---|---|
layers (default) | Mermaid | Components grouped into layer subgraphs with dependency edges |
dependencies | Mermaid | Simplified layer-to-layer dependency flow with edge counts |
dot | GraphViz | Same as layers in DOT format |
dot-dependencies | GraphViz | Same as dependencies in DOT format |
Mermaid Layer Diagram
Shows each real architectural component inside its layer subgraph. Dependency edges are drawn between components; edges that violate layer boundaries are marked as violations.
boundary diagram .
boundary diagram . --diagram-type layers
Example output:
flowchart TB
subgraph Domain
domain__user_User["User"]
domain__user_UserRepository["UserRepository"]
end
subgraph Infrastructure
infra__postgres_PostgresUserRepository["PostgresUserRepository"]
end
infra__postgres_PostgresUserRepository --> domain__user_User
Violation edges are rendered with a dashed arrow and a violation label:
domain__user_User -.->|"infra/postgres (violation)"| infra__postgres_PostgresUserRepository
Note: Only real named components (structs, interfaces, classes) appear in diagrams. Synthetic graph nodes used for internal dependency tracking (
<file>,<package>) are automatically filtered out.
Rendering
Paste the output into any Mermaid-compatible renderer:
- mermaid.live — instant online preview
- GitHub Markdown — wrap in a
```mermaidcode block - VS Code — Mermaid Preview extension
Mermaid Dependency Flow
A higher-level view showing layer-to-layer edges with dependency counts, useful for quickly spotting which layers are talking to each other and where violations are concentrated.
boundary diagram . --diagram-type dependencies
Example output:
flowchart LR
domain["domain (2)"]
infrastructure["infrastructure (1)"]
infrastructure -->|"1 deps"| domain
GraphViz DOT Diagrams
The dot and dot-dependencies types produce GraphViz DOT output. Pipe to dot to render
as an image:
# Render as PNG
boundary diagram . --diagram-type dot | dot -Tpng -o architecture.png
# Render as SVG
boundary diagram . --diagram-type dot-dependencies | dot -Tsvg -o flow.svg
# Save DOT source for later
boundary diagram . --diagram-type dot > architecture.dot
Layer subgraphs use colour-coded backgrounds:
| Layer | Background Colour |
|---|---|
| Domain | #e8f5e9 (green tint) |
| Application | #e3f2fd (blue tint) |
| Infrastructure | #fff3e0 (amber tint) |
| Presentation | #fce4ec (pink tint) |
CI Integration
Generate and commit diagrams as part of a CI workflow:
- name: Update architecture diagram
run: boundary diagram . > docs/architecture.mmd
Or use the diagram as a visual diff in pull requests by generating it and including it in PR descriptions.
Monorepo Support
Boundary supports analyzing monorepos with multiple services, shared modules, and per-service architecture rules.
Per-Service Analysis
Use the --per-service flag to analyze each service independently:
boundary analyze . --per-service
This produces a separate report for each service discovered under the configured services pattern, plus an aggregate summary.
Configuring the Services Pattern
Tell Boundary where your services live:
[project]
services_pattern = "services/*"
This matches directories like services/auth/, services/billing/, services/notifications/, etc. Each is analyzed as an independent unit with its own scores.
Per-Service Layer Overrides
Each service may have its own internal structure. Use layer overrides to configure patterns per-service:
# Global defaults
[layers]
domain = ["**/domain/**"]
application = ["**/application/**"]
infrastructure = ["**/infrastructure/**"]
# Auth service has a different structure
[[layers.overrides]]
scope = "services/auth/**"
domain = ["services/auth/core/**"]
infrastructure = ["services/auth/server/**", "services/auth/adapters/**"]
# Shared modules
[[layers.overrides]]
scope = "common/modules/*/**"
domain = ["common/modules/*/domain/**"]
application = ["common/modules/*/app/**"]
Cross-Service Dependencies
When analyzing the full monorepo (without --per-service), Boundary tracks dependencies between services. Cross-service dependencies that violate layer rules are flagged, helping enforce clean boundaries at service boundaries.
Shared Modules
Shared modules (e.g., common/, pkg/) that are used across multiple services can be configured as cross-cutting concerns if they don’t belong to any specific layer:
[layers]
cross_cutting = ["common/utils/**", "pkg/logger/**"]
Or given their own layer overrides if they contain domain logic:
[[layers.overrides]]
scope = "common/modules/users/**"
domain = ["common/modules/users/domain/**"]
application = ["common/modules/users/app/**"]
CI Integration
Boundary is designed for CI/CD pipelines. Use boundary check to get a pass/fail exit code based on your configured thresholds.
Exit Codes
| Code | Meaning |
|---|---|
0 | Pass – no violations at or above the failure threshold |
1 | Fail – violations found at or above the failure threshold |
GitHub Actions
name: Architecture Check
on:
pull_request:
branches: [main]
jobs:
boundary:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Boundary
run: |
curl -fsSL https://github.com/rebelopsio/boundary/releases/latest/download/boundary-x86_64-unknown-linux-gnu.tar.gz \
| tar xz -C /usr/local/bin
- name: Check Architecture
run: boundary check . --format json --fail-on error
Configuration Options
Failure Threshold
Control which violation severity causes a non-zero exit:
# Fail on errors only (default)
boundary check . --fail-on error
# Fail on warnings and errors
boundary check . --fail-on warning
# Fail on everything including info
boundary check . --fail-on info
Or set it in .boundary.toml:
[rules]
fail_on = "error"
Minimum Score
Fail if the overall architecture score drops below a threshold:
[rules]
min_score = 70.0
JSON Output
Use --format json for machine-readable output that other tools can consume:
boundary check . --format json
Ignoring Rules
Suppress specific violations by rule ID using --ignore. This is useful for known false
positives or rules that don’t apply to certain projects:
# Ignore missing-port warnings (e.g. for DTOs and utilities)
boundary check . --ignore PA001
# Ignore multiple rules
boundary check . --ignore PA001,L005
Ignored violations are excluded before the pass/fail decision, so they won’t cause CI failures. See Rules & Rule IDs for the full rule catalog.
Evolution Tracking
Track architecture scores over time:
# Save a snapshot after each successful check
boundary check . --track
# Fail if the score regresses from the last snapshot
boundary check . --no-regression
Snapshots are stored in .boundary/ and can be committed to your repository to track trends.
GitLab CI
architecture:
stage: test
image: rust:latest
script:
- cargo install --git https://github.com/rebelopsio/boundary boundary
- boundary check . --format json --fail-on error
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Pre-commit Hook
Run Boundary as a pre-commit check:
#!/bin/sh
# .git/hooks/pre-commit
boundary check . --fail-on error --compact
Editor Integration
Boundary ships a Language Server Protocol (LSP) server — boundary-lsp — that brings architectural violation detection directly into your editor as you code.
What It Does
- Inline diagnostics — layer boundary violations, missing ports, and other violations appear as errors and warnings on the offending import lines
- Hover info — hover over any type to see its architectural layer classification
- Live feedback — re-analyzes on every file save so diagnostics stay current
Installation
boundary-lsp is distributed alongside the main boundary binary. If you installed via Homebrew, it is already available:
which boundary-lsp
If you installed from source, build it with:
cargo install --git https://github.com/rebelopsio/boundary boundary-lsp
Editor Setup
Neovim
The recommended way is boundary.nvim, a dedicated plugin that provides LSP integration, commands, and statusline support. Requires Neovim 0.11+.
lazy.nvim:
{
"rebelopsio/boundary.nvim",
opts = {},
}
This gives you inline diagnostics, hover info, and commands like :BoundaryAnalyze, :BoundaryScore, :BoundaryCheck, and :BoundaryDiagram. See the boundary.nvim README for the full feature list and configuration options.
Manual setup (nvim-lspconfig, Neovim < 0.11):
local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")
if not configs.boundary then
configs.boundary = {
default_config = {
cmd = { "boundary-lsp" },
filetypes = { "go", "rust", "typescript", "java" },
root_dir = lspconfig.util.root_pattern(".boundary.toml", ".git"),
single_file_support = false,
},
}
end
lspconfig.boundary.setup({})
VS Code
Install the Boundary extension from the VS Code Marketplace. It manages boundary-lsp automatically.
To configure manually, add to your settings.json:
{
"boundary.lsp.enable": true,
"boundary.lsp.path": "boundary-lsp"
}
Helix
Add to ~/.config/helix/languages.toml:
[[language]]
name = "go"
language-servers = ["boundary-lsp"]
[[language]]
name = "rust"
language-servers = ["boundary-lsp"]
[language-server.boundary-lsp]
command = "boundary-lsp"
Emacs (eglot)
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'((go-mode go-ts-mode) . ("boundary-lsp"))))
How It Works
boundary-lsp runs boundary’s analysis pipeline in the background using the project’s .boundary.toml configuration. On initialization and after each file save, it re-analyzes the project and publishes LSP diagnostics mapped to the exact import lines that cause violations.
The server auto-detects languages from file extensions, so no additional configuration is needed beyond what your .boundary.toml already defines.
CLI Reference
Global Options
boundary [COMMAND]
Options:
-h, --help Print help
-V, --version Print version
Commands
boundary analyze
Analyze a codebase and print a full architecture report.
boundary analyze [OPTIONS] <PATH>
Arguments:
<PATH> Path to the project root
Options:
-c, --config <CONFIG> Config file path (defaults to .boundary.toml in project root)
--format <FORMAT> Output format [default: text] [possible values: text, json, markdown]
--compact Compact output (single-line JSON, no colors for text)
--languages <LANGUAGES> Languages to analyze (auto-detect if not specified)
--incremental Use incremental analysis (cache unchanged files)
--per-service Analyze each service independently (monorepo support)
--ignore <RULES> Ignore specific rule IDs (comma-separated, e.g. PA001,L005)
Examples:
# Analyze current directory
boundary analyze .
# JSON output for a specific project
boundary analyze /path/to/project --format json
# Analyze only Go files with incremental caching
boundary analyze . --languages go --incremental
# Per-service monorepo analysis
boundary analyze . --per-service
# Suppress missing-port warnings
boundary analyze . --ignore PA001
boundary check
Analyze and exit with code 0 (pass) or 1 (fail). Designed for CI pipelines.
boundary check [OPTIONS] <PATH>
Arguments:
<PATH> Path to the project root
Options:
--fail-on <FAIL_ON> Minimum severity to cause failure [default: error]
-c, --config <CONFIG> Config file path
--format <FORMAT> Output format [default: text] [possible values: text, json, markdown]
--compact Compact output (single-line JSON, no colors for text)
--languages <LANGUAGES> Languages to analyze (auto-detect if not specified)
--track Save analysis snapshot for evolution tracking
--no-regression Fail if architecture score regresses from last snapshot
--incremental Use incremental analysis (cache unchanged files)
--per-service Analyze each service independently (monorepo support)
--ignore <RULES> Ignore specific rule IDs (comma-separated, e.g. PA001,L005)
Examples:
# CI check with JSON output
boundary check . --format json --fail-on error
# Track architecture evolution
boundary check . --track --no-regression
# Ignore false-positive missing-port warnings in CI
boundary check . --ignore PA001
boundary init
Create a default .boundary.toml configuration file in the current directory.
boundary init [OPTIONS]
Options:
--force Overwrite existing config
Examples:
# Create config (fails if .boundary.toml already exists)
boundary init
# Overwrite existing config
boundary init --force
boundary diagram
Generate an architecture diagram in Mermaid or GraphViz DOT format.
boundary diagram [OPTIONS] <PATH>
Arguments:
<PATH> Path to the project root
Options:
-c, --config <CONFIG> Config file path
--diagram-type <DIAGRAM_TYPE> Diagram type [default: layers]
[possible values: layers, dependencies, dot, dot-dependencies]
--languages <LANGUAGES> Languages to analyze (auto-detect if not specified)
Diagram types:
| Type | Format | Description |
|---|---|---|
layers | Mermaid | Layer-grouped component diagram |
dependencies | Mermaid | Component dependency graph |
dot | GraphViz DOT | Layer diagram in DOT format |
dot-dependencies | GraphViz DOT | Dependency graph in DOT format |
Examples:
# Mermaid layer diagram
boundary diagram .
# GraphViz DOT dependency graph, save to file
boundary diagram . --diagram-type dot-dependencies > architecture.dot
boundary forensics
Generate a detailed forensics report for a specific module with DDD pattern analysis.
boundary forensics [OPTIONS] <PATH>
Arguments:
<PATH> Path to the module directory
Options:
--project-root <PROJECT_ROOT> Project root (auto-detected if not specified)
-c, --config <CONFIG> Config file path
--languages <LANGUAGES> Languages to analyze (auto-detect if not specified)
-o, --output <OUTPUT> Write output to file instead of stdout
The forensics report includes:
- Per-aggregate analysis with fields and method signatures
- Domain event detection (structs ending with
Event) - Value object heuristics (structs without identity fields)
- Import classification (stdlib, internal, external)
- Dependency audit with infrastructure leak detection
- Port/adapter mapping with interface coverage
- Improvement suggestions (anemic models, missing events, unmatched ports)
Examples:
# Analyze a specific module
boundary forensics internal/domain/billing
# Save report to markdown file
boundary forensics internal/domain/billing -o report.md
# Specify project root explicitly
boundary forensics services/auth/core --project-root /path/to/monorepo