Skip to content
Arxo Arxo

Layer Enforcement

Architectural layers organize code into logical boundaries where each layer has specific responsibilities and dependency rules. This guide shows you how to define and enforce layers using Arxo.

Layers are horizontal slices of your architecture where:

  • Each layer has a clear purpose (UI, business logic, data access)
  • Dependencies flow in one direction (top to bottom)
  • Lower layers don’t know about upper layers

Example:

┌─────────────────┐
│ UI Layer │ ← Can depend on Services
├─────────────────┤
│ Services Layer │ ← Can depend on Data
├─────────────────┤
│ Data Layer │ ← Cannot depend on anything
└─────────────────┘

Why it matters:

  • Maintainability - Clear separation of concerns
  • Testability - Lower layers testable in isolation
  • Flexibility - Swap implementations without breaking upper layers
  • Onboarding - New developers understand structure quickly

Use the layer_violations metric:

Terminal window
arxo analyze --metric layer_violations

This metric requires explicit architecture.layers configuration. If missing, Arxo returns a config error.

Use the canonical config shape shown below:

  • define layers under architecture.layers
  • use paths (array of globs), not path
  • set can_depend_on to layer names, not path globs

Create .arxo.yaml with layer definitions:

architecture:
layers:
# Define layers from top to bottom
- name: presentation
paths: ["src/ui/**"]
allowed_effects: []
can_depend_on: ["business", "shared"]
- name: business
paths: ["src/services/**"]
allowed_effects: []
can_depend_on: ["data", "shared"]
- name: data
paths: ["src/data/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: shared
paths: ["src/shared/**"]
allowed_effects: []
can_depend_on: [] # No dependencies
policy:
invariants:
- metric: layer_violations.violations_count
op: "=="
value: 0
message: "Layer violations detected - see report for details"
PropertyDescriptionExample
nameLayer identifier"presentation"
pathsGlob patterns matching files in the layer["src/ui/**"]
can_depend_onAllowed target layer names["business", "shared"]
allowed_effectsAllowed effect kinds in this layer[]

Classic web application structure:

architecture:
layers:
- name: presentation
paths: ["src/controllers/**"]
allowed_effects: []
can_depend_on: ["business_logic"]
- name: business_logic
paths: ["src/services/**"]
allowed_effects: []
can_depend_on: ["data_access", "models"]
- name: data_access
paths: ["src/repositories/**"]
allowed_effects: []
can_depend_on: ["models"]
- name: models
paths: ["src/models/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

Rules:

  • Controllers → Services → Repositories → Models
  • No upward dependencies
  • Models are pure (no dependencies)

Concentric circles with dependency inversion:

architecture:
layers:
- name: frameworks
paths: ["src/frameworks/**"]
allowed_effects: []
can_depend_on: ["adapters", "use_cases", "domain"]
- name: adapters
paths: ["src/adapters/**"]
allowed_effects: []
can_depend_on: ["use_cases", "domain"]
- name: use_cases
paths: ["src/use-cases/**"]
allowed_effects: []
can_depend_on: ["domain"]
- name: domain
paths: ["src/domain/**"]
allowed_effects: []
can_depend_on: [] # Pure business logic
metrics:
- id: layer_violations
enabled: true

Key principle: Inner layers (domain) don’t depend on outer layers (frameworks).


3. Hexagonal Architecture (Ports & Adapters)

Section titled “3. Hexagonal Architecture (Ports & Adapters)”

Domain at the center, adapters on the outside:

architecture:
layers:
- name: adapters
paths: ["src/adapters/**"]
allowed_effects: []
can_depend_on: ["ports", "domain"]
- name: application
paths: ["src/application/**"]
allowed_effects: []
can_depend_on: ["ports", "domain"]
- name: ports
paths: ["src/ports/**"]
allowed_effects: []
can_depend_on: ["domain"]
- name: domain
paths: ["src/domain/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

Adapters (HTTP, DB) implement ports (interfaces) defined by the domain.


Vertical slices with horizontal layers:

architecture:
layers:
- name: app
paths: ["src/app/**"]
allowed_effects: []
can_depend_on: ["pages", "widgets", "features", "entities", "shared"]
- name: pages
paths: ["src/pages/**"]
allowed_effects: []
can_depend_on: ["widgets", "features", "entities", "shared"]
- name: widgets
paths: ["src/widgets/**"]
allowed_effects: []
can_depend_on: ["features", "entities", "shared"]
- name: features
paths: ["src/features/**"]
allowed_effects: []
can_depend_on: ["entities", "shared"]
- name: entities
paths: ["src/entities/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: shared
paths: ["src/shared/**"]
allowed_effects: []
can_depend_on: []

FSD hierarchy: app → pages → widgets → features → entities → shared


Enforce service boundaries:

architecture:
layers:
- name: api_gateway
paths: ["services/api-gateway/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: auth_service
paths: ["services/auth/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: user_service
paths: ["services/users/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: shared
paths: ["packages/shared/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true
policy:
invariants:
- metric: layer_violations.violations_count
op: "=="
value: 0
message: "Services should not depend on each other directly"

Rule: Services only depend on shared packages, never on each other.


Structure:

src/
components/ # UI components
hooks/ # React hooks
services/ # API calls
utils/ # Pure functions
types/ # TypeScript types

Configuration:

architecture:
layers:
- name: components
paths: ["src/components/**"]
allowed_effects: []
can_depend_on: ["hooks", "services", "utils", "types"]
- name: hooks
paths: ["src/hooks/**"]
allowed_effects: []
can_depend_on: ["services", "utils", "types"]
- name: services
paths: ["src/services/**"]
allowed_effects: []
can_depend_on: ["utils", "types"]
- name: utils
paths: ["src/utils/**"]
allowed_effects: []
can_depend_on: ["types"]
- name: types
paths: ["src/types/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

Structure:

src/
routes/ # HTTP routes
controllers/ # Request handlers
services/ # Business logic
repositories/ # Database access
models/ # Data models
middleware/ # Express middleware

Configuration:

architecture:
layers:
- name: routes
paths: ["src/routes/**"]
allowed_effects: []
can_depend_on: ["controllers", "middleware"]
- name: controllers
paths: ["src/controllers/**"]
allowed_effects: []
can_depend_on: ["services", "models"]
- name: services
paths: ["src/services/**"]
allowed_effects: []
can_depend_on: ["repositories", "models"]
- name: repositories
paths: ["src/repositories/**"]
allowed_effects: []
can_depend_on: ["models"]
- name: middleware
paths: ["src/middleware/**"]
allowed_effects: []
can_depend_on: ["models"]
- name: models
paths: ["src/models/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

Key rule: Controllers never directly access repositories (must go through services).


Structure:

myapp/
views/ # View layer
services/ # Business logic
models/ # ORM models
serializers/ # Data serialization
utils/ # Utilities

Configuration:

architecture:
layers:
- name: views
paths: ["myapp/views/**"]
allowed_effects: []
can_depend_on: ["services", "serializers"]
- name: services
paths: ["myapp/services/**"]
allowed_effects: []
can_depend_on: ["models", "utils"]
- name: serializers
paths: ["myapp/serializers/**"]
allowed_effects: []
can_depend_on: ["models"]
- name: models
paths: ["myapp/models/**"]
allowed_effects: []
can_depend_on: []
- name: utils
paths: ["myapp/utils/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

Allow lower layers to define interfaces that upper layers implement:

architecture:
layers:
- name: infrastructure
paths: ["src/infrastructure/**"]
allowed_effects: []
can_depend_on: ["ports", "domain"] # Implements interfaces
- name: application
paths: ["src/application/**"]
allowed_effects: []
can_depend_on: ["ports", "domain"]
- name: ports
paths: ["src/application/ports/**"]
allowed_effects: []
can_depend_on: ["domain"]
- name: domain
paths: ["src/domain/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

Infrastructure depends on ports (interfaces), not the application itself.


Combine vertical (feature) and horizontal (technical) boundaries:

architecture:
layers:
# Horizontal layers within each feature
- name: auth_ui
paths: ["src/features/auth/ui/**"]
allowed_effects: []
can_depend_on: ["auth_logic"]
- name: auth_logic
paths: ["src/features/auth/logic/**"]
allowed_effects: []
can_depend_on: ["auth_data"]
- name: auth_data
paths: ["src/features/auth/data/**"]
allowed_effects: []
can_depend_on: []
# Same for user feature
- name: user_ui
paths: ["src/features/user/ui/**"]
allowed_effects: []
can_depend_on: ["user_logic"]
- name: user_logic
paths: ["src/features/user/logic/**"]
allowed_effects: []
can_depend_on: ["user_data"]
- name: user_data
paths: ["src/features/user/data/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

The current architecture schema is allowlist-based (can_depend_on). Model explicit bans by omitting forbidden layer names:

architecture:
layers:
- name: frontend
paths: ["src/frontend/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: backend
paths: ["src/backend/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: shared
paths: ["src/shared/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

policy:
invariants:
- metric: layer_violations.violations_count
op: "=="
value: 0
message: "Layer violations are not allowed"

For legacy codebases:

policy:
invariants:
# Week 1: Establish baseline
- metric: layer_violations.violations_count
op: "<="
value: 50
message: "Reduce violations to 50"
# Week 4: Tighten
- metric: layer_violations.violations_count
op: "<="
value: 20
message: "Reduce violations to 20"
# Goal: Eventually zero
policy:
invariants:
- metric: layer_violations.violations_count
op: "=="
value: 0
message: "No severe violations (e.g., data layer importing UI)"
- metric: layer_violations.violations_count
op: "<="
value: 10
message: "Minor violations allowed temporarily"

name: Layer Enforcement
on:
pull_request:
branches: [main]
jobs:
check-layers:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Arxo
run: |
curl -sL "https://github.com/arxohq/arxo/releases/latest/download/arxo-linux-x86_64.tar.gz" | tar xz
chmod +x arxo && sudo mv arxo /usr/local/bin/
- name: Check Layer Violations
run: |
arxo analyze --metric layer_violations --fail-fast
.git/hooks/pre-commit
#!/bin/bash
arxo analyze --metric layer_violations --quiet
if [ $? -ne 0 ]; then
echo "❌ Layer violations detected!"
echo "Run: arxo analyze --metric layer_violations"
exit 1
fi

Cause: Starting with strict rules on legacy code.

Solution: Use progressive reduction:

# Start loose, tighten over time
policy:
invariants:
- metric: layer_violations.violations_count
op: "<="
value: 100 # Current state

Fix violations incrementally, reduce threshold each sprint.


Cause: Incorrect glob patterns.

Solution: Test with --format json:

Terminal window
arxo analyze --metric layer_violations --format json | \
jq '.results[] | select(.id=="layer_violations") | .evidence'

Check which files are in each layer.


Cause: Legitimate cross-cutting concerns (logging, config).

Solution: Add exceptions:

architecture:
layers:
- name: services
paths: ["src/services/**"]
allowed_effects: []
can_depend_on: ["data", "shared_logging", "shared_config"]
- name: data
paths: ["src/data/**"]
allowed_effects: []
can_depend_on: []
- name: shared_logging
paths: ["src/shared/logging/**"]
allowed_effects: []
can_depend_on: []
- name: shared_config
paths: ["src/shared/config/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

Or exclude from analysis:

data:
import_graph:
exclude:
- "**/shared/logging/**"
- "**/shared/config/**"

Cause: Features importing from each other.

Solution: Extract shared logic:

src/
features/
auth/
users/
shared/ # Extract common code here
types/
utils/
architecture:
layers:
- name: auth
paths: ["src/features/auth/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: users
paths: ["src/features/users/**"]
allowed_effects: []
can_depend_on: ["shared"]
- name: shared
paths: ["src/shared/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true

Generate HTML report to see violation details:

Terminal window
arxo analyze --metric layer_violations --format html --output report.html
open report.html

The report shows:

  • Which files violate which layers
  • Dependency paths causing violations
  • Suggested refactorings

Layer violations often correlate with other issues:

metrics:
- id: layer_violations
- id: scc # Cycles across layers
- id: propagation_cost # Tight coupling
- id: hierarchy # Inferred hierarchy erosion signals
policy:
invariants:
- metric: layer_violations.violations_count
op: "=="
value: 0
- metric: scc.max_cycle_size
op: "=="
value: 0
- metric: hierarchy.module.edge.upward_weight_ratio
op: "<="
value: 0.05

layer_violations enforces explicit rules from architecture.layers, while hierarchy tracks inferred structural drift (upward/skip-layer patterns) even without strict layer rules.

Use both metrics together:

  • layer_violations checks whether layer-to-layer dependencies respect can_depend_on.
  • effect_violations checks whether direct/transitive side effects respect allowed_effects and optional architecture.effect_rules.

Typical split:

  • Dependency direction and boundary shape: layer_violations.violations_count
  • Side-effect containment and purity boundaries: effect_violations.violation_count

Example combined gate:

metrics:
- id: layer_violations
- id: effect_violations
policy:
invariants:
- metric: layer_violations.violations_count
op: "=="
value: 0
- metric: effect_violations.violation_count
op: "=="
value: 0

  1. Start Simple - Define 3-4 layers initially, add more later
  2. Document Layers - Explain purpose and rules in README
  3. Gradual Enforcement - Use progressive policies for legacy code
  4. Shared Layer - Create a “shared” layer for cross-cutting concerns
  5. CI Integration - Block PRs with violations
  6. Team Education - Explain why layers matter, not just what they are
  7. Regular Reviews - Revisit layer definitions as architecture evolves

❌ controllers/UserController.ts imports models/User.ts
❌ models/User.ts imports services/AuthService.ts
❌ services/AuthService.ts imports controllers/AuthController.ts

Circular dependencies, unclear boundaries.

architecture:
layers:
- name: controllers
paths: ["controllers/**"]
allowed_effects: []
can_depend_on: ["services"]
- name: services
paths: ["services/**"]
allowed_effects: []
can_depend_on: ["models"]
- name: models
paths: ["models/**"]
allowed_effects: []
can_depend_on: []
metrics:
- id: layer_violations
enabled: true
✅ controllers/UserController.ts → services/UserService.ts
✅ services/UserService.ts → models/User.ts
✅ models/User.ts → (no dependencies)

Clear hierarchy, testable, maintainable.