Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Parse – Build ASTs for each source file using tree-sitter
  2. Extract – Identify components (interfaces, structs, imports, dependencies)
  3. Classify – Assign components to architectural layers (Domain, Application, Infrastructure, Presentation)
  4. Build Graph – Construct a dependency graph with layer metadata using petgraph
  5. Analyze – Detect violations and calculate scores
  6. 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 threshold
  • 1 — 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]

KeyTypeDefaultDescription
languageslist[] (auto-detect)Languages to analyze. Options: go, rust, typescript, java
exclude_patternslist["vendor/**", "**/*_test.go", "**/testdata/**"]Glob patterns for files to skip
services_patternstring(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.

KeyDefault Patterns
domain**/domain/**, **/entity/**, **/model/**
application**/application/**, **/usecase/**, **/service/**
infrastructure**/infrastructure/**, **/adapter/**, **/repository/**, **/persistence/**
presentation**/presentation/**, **/handler/**, **/api/**, **/cmd/**

Additional fields:

KeyTypeDescription
cross_cuttinglistPaths exempt from layer violation checks (applies to both source files and import targets)
architecture_modestringGlobal 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]

KeyDefaultDescription
layer_isolation_weight0.4Weight for layer isolation score
dependency_direction_weight0.4Weight for dependency direction score
interface_coverage_weight0.2Weight for interface coverage score

Weights should sum to 1.0.

[rules]

KeyTypeDefaultDescription
fail_onstring"error"Minimum severity to cause non-zero exit
min_scorefloat(none)Optional minimum overall score
detect_init_functionsbooltrueDetect 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 NameDefault SeverityDescription
layer_boundaryerrorInner layer depends on outer layer
circular_dependencyerrorCircular dependency between components
missing_portwarningAdapter without a corresponding port interface
constructor_concretewarningConstructor returns concrete type instead of port
init_couplingwarningGo init() function creates hidden coupling
domain_infra_leakerrorDomain 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/**"]
KeyTypeDescription
rulestringRule ID to suppress (e.g., PA001, L001)
pathslistGlob 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"
KeyDescription
nameRule identifier
from_patternGlob for the source of the dependency
to_patternGlob for the target of the dependency
action"deny" (only option currently)
severity"error", "warning", or "info"
messageCustom 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:

LayerPurposeDefault Patterns
DomainCore business logic, entities, value objects**/domain/**, **/entity/**, **/model/**
ApplicationUse cases, application services, orchestration**/application/**, **/usecase/**, **/service/**
InfrastructureDatabase adapters, external APIs, persistence**/infrastructure/**, **/adapter/**, **/repository/**, **/persistence/**
PresentationHTTP 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):

ScoreDefault WeightWhat It Measures
Layer Conformance40%How closely each package’s (A, I) values match its assigned layer’s expected region
Dependency Compliance40%Fraction of cross-layer imports that flow in the correct direction
Interface Coverage20%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, or Service

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)

IDNameDescriptionSeverity
L001domain-depends-on-infrastructureDomain layer imports directly from infrastructureError
L002domain-depends-on-applicationDomain layer depends on application orchestrationError
L003application-bypasses-portsApplication layer calls infrastructure without a portError
L004init-function-couplingInit/main wiring function couples layers directlyWarning
L005domain-uses-infrastructure-typeDomain code references an infrastructure typeError
L099layer-boundary-violationCatch-all for other forbidden layer crossingsError

Dependency Violations (D)

IDNameDescriptionSeverity
D001circular-dependencyCircular dependency detected between componentsError

Port/Adapter Violations (PA)

IDNameDescriptionSeverity
PA001missing-port-interfaceInfrastructure adapter has no matching domain portWarning
PA003constructor-returns-concrete-typeConstructor returns concrete type instead of port interfaceWarning

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"
FieldRequiredDescription
nameYesUnique identifier shown in violation output
from_patternYesRegex matched against the source component’s path
to_patternYesRegex matched against the import path of the dependency
actionNoOnly "deny" is supported (default: "deny")
severityNo"error", "warning", or "info" (default: "error")
messageNoCustom 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):

ZoneConditionMeaning
⚠ PainA < 0.5 and I < 0.5Concrete and stable — rigid, hard to change
⚠ UselessnessA > 0.5 and I > 0.5Abstract and unstable — unused abstractions
otherwiseOn 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:

FieldDescription
scoreArchitecture score dimensions (omitted if pattern confidence < 0.5)
violationsArray of all violations
component_countTotal number of real components
dependency_countTotal number of dependency edges
files_analyzedNumber of source files analyzed
metricsDetailed metrics breakdown
package_metricsArray of per-package A/I/D metrics
pattern_detectionPattern confidence distribution

Each violation object includes:

FieldDescription
ruleStable rule ID (e.g. L001, PA001, D001)
rule_nameHuman-readable rule name (e.g. domain-depends-on-infrastructure)
kindViolation kind with structured details
severityerror, warning, or info
locationFile path, line, and column
messageHuman-readable description
suggestionFix 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

TypeFormatDescription
layers (default)MermaidComponents grouped into layer subgraphs with dependency edges
dependenciesMermaidSimplified layer-to-layer dependency flow with edge counts
dotGraphVizSame as layers in DOT format
dot-dependenciesGraphVizSame 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 ```mermaid code 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:

LayerBackground 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

CodeMeaning
0Pass – no violations at or above the failure threshold
1Fail – 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:

TypeFormatDescription
layersMermaidLayer-grouped component diagram
dependenciesMermaidComponent dependency graph
dotGraphViz DOTLayer diagram in DOT format
dot-dependenciesGraphViz DOTDependency 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