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

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
PA002port-without-implementationDomain port has no infrastructure adapter implementing itInfo
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.

PA002: port-without-implementation

Detects domain-layer port interfaces that have no matching infrastructure adapter. This helps identify ports that may have been defined but never implemented, or whose adapter was removed.

Default severity is Info because unimplemented ports may be planned, implemented in a separate module, or defined as part of an interface-first design approach.

Violation:

// domain/ports/audit.go
type AuditLogger interface {
    Log(event string) error
}
// No adapter implementing AuditLogger exists in the infrastructure layer

Fix: Create an infrastructure adapter:

// infrastructure/logging/audit.go
type fileAuditLogger struct { path string }

func NewFileAuditLogger(path string) ports.AuditLogger {
    return &fileAuditLogger{path: path}
}

PA002 checks both explicit implements relationships (from constructor analysis) and name-heuristic matching (same logic as PA001, inverted).

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 | ... |