Layer Enforcement
Layer Enforcement
Section titled “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.
What Are Architectural Layers?
Section titled “What Are Architectural Layers?”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
Detecting Layer Violations
Section titled “Detecting Layer Violations”Use the layer_violations metric:
arxo analyze --metric layer_violationsThis 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), notpath - set
can_depend_onto layer names, not path globs
Defining Layers
Section titled “Defining Layers”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"Layer Properties
Section titled “Layer Properties”| Property | Description | Example |
|---|---|---|
name | Layer identifier | "presentation" |
paths | Glob patterns matching files in the layer | ["src/ui/**"] |
can_depend_on | Allowed target layer names | ["business", "shared"] |
allowed_effects | Allowed effect kinds in this layer | [] |
Common Layer Patterns
Section titled “Common Layer Patterns”1. Three-Tier Architecture (Web Apps)
Section titled “1. Three-Tier Architecture (Web Apps)”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: trueRules:
- Controllers → Services → Repositories → Models
- No upward dependencies
- Models are pure (no dependencies)
2. Clean Architecture (Uncle Bob)
Section titled “2. Clean Architecture (Uncle Bob)”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: trueKey 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: trueAdapters (HTTP, DB) implement ports (interfaces) defined by the domain.
4. Feature-Sliced Design (FSD)
Section titled “4. Feature-Sliced Design (FSD)”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
5. Microservices Monorepo
Section titled “5. Microservices Monorepo”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.
Real-World Examples
Section titled “Real-World Examples”Example 1: React Application
Section titled “Example 1: React Application”Structure:
src/ components/ # UI components hooks/ # React hooks services/ # API calls utils/ # Pure functions types/ # TypeScript typesConfiguration:
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: trueExample 2: Express.js API
Section titled “Example 2: Express.js API”Structure:
src/ routes/ # HTTP routes controllers/ # Request handlers services/ # Business logic repositories/ # Database access models/ # Data models middleware/ # Express middlewareConfiguration:
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: trueKey rule: Controllers never directly access repositories (must go through services).
Example 3: Python Django Application
Section titled “Example 3: Python Django Application”Structure:
myapp/ views/ # View layer services/ # Business logic models/ # ORM models serializers/ # Data serialization utils/ # UtilitiesConfiguration:
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: trueAdvanced Patterns
Section titled “Advanced Patterns”Dependency Inversion
Section titled “Dependency Inversion”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: trueInfrastructure depends on ports (interfaces), not the application itself.
Multiple Boundaries
Section titled “Multiple Boundaries”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: trueForbidden Imports
Section titled “Forbidden Imports”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: truePolicy Enforcement
Section titled “Policy Enforcement”Zero Violations (Strict)
Section titled “Zero Violations (Strict)”policy: invariants: - metric: layer_violations.violations_count op: "==" value: 0 message: "Layer violations are not allowed"Progressive Reduction (Pragmatic)
Section titled “Progressive Reduction (Pragmatic)”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 zeroSeverity-Based
Section titled “Severity-Based”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"CI Integration
Section titled “CI Integration”GitHub Actions
Section titled “GitHub Actions”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-fastPre-commit Hook
Section titled “Pre-commit Hook”#!/bin/basharxo analyze --metric layer_violations --quiet
if [ $? -ne 0 ]; then echo "❌ Layer violations detected!" echo "Run: arxo analyze --metric layer_violations" exit 1fiTroubleshooting
Section titled “Troubleshooting””Too Many Violations”
Section titled “”Too Many Violations””Cause: Starting with strict rules on legacy code.
Solution: Use progressive reduction:
# Start loose, tighten over timepolicy: invariants: - metric: layer_violations.violations_count op: "<=" value: 100 # Current stateFix violations incrementally, reduce threshold each sprint.
”Path Patterns Not Matching”
Section titled “”Path Patterns Not Matching””Cause: Incorrect glob patterns.
Solution: Test with --format json:
arxo analyze --metric layer_violations --format json | \ jq '.results[] | select(.id=="layer_violations") | .evidence'Check which files are in each layer.
”False Positives”
Section titled “”False Positives””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: trueOr exclude from analysis:
data: import_graph: exclude: - "**/shared/logging/**" - "**/shared/config/**"“Violations Across Features”
Section titled ““Violations Across Features””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: trueVisualization
Section titled “Visualization”Generate HTML report to see violation details:
arxo analyze --metric layer_violations --format html --output report.htmlopen report.htmlThe report shows:
- Which files violate which layers
- Dependency paths causing violations
- Suggested refactorings
Combining with Other Metrics
Section titled “Combining with Other Metrics”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.05layer_violations enforces explicit rules from architecture.layers, while hierarchy
tracks inferred structural drift (upward/skip-layer patterns) even without strict layer rules.
Layer Violations vs Effect Violations
Section titled “Layer Violations vs Effect Violations”Use both metrics together:
layer_violationschecks whether layer-to-layer dependencies respectcan_depend_on.effect_violationschecks whether direct/transitive side effects respectallowed_effectsand optionalarchitecture.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: 0Best Practices
Section titled “Best Practices”- Start Simple - Define 3-4 layers initially, add more later
- Document Layers - Explain purpose and rules in README
- Gradual Enforcement - Use progressive policies for legacy code
- Shared Layer - Create a “shared” layer for cross-cutting concerns
- CI Integration - Block PRs with violations
- Team Education - Explain why layers matter, not just what they are
- Regular Reviews - Revisit layer definitions as architecture evolves
Real-World Migration
Section titled “Real-World Migration”Before (No Layers)
Section titled “Before (No Layers)”❌ controllers/UserController.ts imports models/User.ts❌ models/User.ts imports services/AuthService.ts❌ services/AuthService.ts imports controllers/AuthController.tsCircular dependencies, unclear boundaries.
After (With Layers)
Section titled “After (With Layers)”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.
Next Steps
Section titled “Next Steps”- Breaking Cycles - Fix circular dependencies
- Policy Examples - Real-world policies
- CI Integration - Enforce in pipelines
- Hierarchy Metric - Measure inferred layer-structure erosion
- Effect Violations - Enforce side-effect boundaries
- Troubleshooting - Common issues