Your team spent two weeks designing the new module boundary. You wrote an Architecture Decision Record. You presented it at the engineering all-hands. Everyone nodded. And three sprints later, there are seventeen imports that cross the boundary you defined, four direct database calls from the API layer, and a circular dependency that's going to cost a week to untangle.
Architecture decisions don't fail because people disagree with them. They fail because nobody enforces them at the point where code is actually written.
The enforcement gap
Most teams document architectural decisions in one of two ways: ADRs (Architecture Decision Records) stored in a docs folder, or tribal knowledge shared in Slack threads and design reviews. Both approaches have the same fundamental problem — they rely on human reviewers remembering the rules and catching violations during review.
This worked when teams were smaller, codebases were simpler, and every PR was written by a human who attended the design review. It breaks down when:
- AI coding assistants generate code that has never been exposed to your architectural decisions
- New team members haven't internalised the unwritten rules yet
- Review fatigue causes experienced reviewers to miss violations in large PRs
- Cross-team PRs touch modules whose architectural constraints the reviewer doesn't know
How autter enforces architecture
autter lets you encode architectural decisions as machine-enforceable rules that are checked on every pull request, automatically.
Module boundary enforcement
Define which modules can import from which, and autter will block PRs that violate the boundaries:
# autter.config.yml
architecture:
boundaries:
# API layer can import from services, not directly from database
- module: "src/api/**"
can_import:
- "src/services/**"
- "src/types/**"
- "src/utils/**"
cannot_import:
- "src/database/**"
- "src/infrastructure/**"
# Services can import from database, not from API
- module: "src/services/**"
can_import:
- "src/database/**"
- "src/types/**"
- "src/utils/**"
cannot_import:
- "src/api/**"
# Shared types must not import from any layer
- module: "src/types/**"
can_import:
- "src/types/**"
cannot_import:
- "src/api/**"
- "src/services/**"
- "src/database/**"When an AI assistant generates code that imports the database client directly in an API route, autter catches it immediately:
// src/api/routes/users.ts
// autter: ARCHITECTURE — direct database import violates layer boundary
// This module (src/api/**) cannot import from src/database/**
// Suggested: import from src/services/user-service instead
import { db } from "../../database/client";
export async function getUser(id: string) {
return db.users.findUnique({ where: { id } });
}Dependency direction rules
Beyond module boundaries, autter enforces dependency direction — ensuring your dependency graph flows in one direction and doesn't develop cycles:
| Rule | What it prevents |
|---|---|
| No circular dependencies | Module A imports B, B imports A |
| Layer direction | Presentation → Business → Data, never reversed |
| Package isolation | Feature packages don't cross-import |
| Shared kernel restriction | Only approved types in the shared layer |
Pattern enforcement
Some architectural decisions aren't about what you import but how you write code. autter can enforce structural patterns:
architecture:
patterns:
# All API routes must use the standard error handler
- name: "api-error-handling"
match: "src/api/routes/**/*.ts"
require:
- pattern: "withErrorHandler"
message: "All API routes must be wrapped in withErrorHandler()"
# Database queries must go through the repository pattern
- name: "repository-pattern"
match: "src/services/**/*.ts"
forbid:
- pattern: "db\\.(select|insert|update|delete)"
message: "Services must use repository methods, not direct DB queries"
# All new events must include a schema version
- name: "event-versioning"
match: "src/events/**/*.ts"
require:
- pattern: "schemaVersion"
message: "All event types must include a schemaVersion field"ADR-linked rules
autter lets you link rules to the ADR that motivated them. When a violation is flagged, the developer sees not just what they did wrong, but why the rule exists:
architecture:
boundaries:
- module: "src/api/**"
cannot_import:
- "src/database/**"
adr: "docs/adr/003-layered-architecture.md"
rationale: >
Direct database access from API routes bypasses business logic
validation. This caused incident INC-2026-041 where an API route
updated user data without triggering the audit log.The PR comment includes:
Architecture violation: Direct import from
src/database/clientin API layer.Why this rule exists: Direct database access from API routes bypasses business logic validation. This caused incident INC-2026-041. See ADR-003 for details.
Suggested fix: Import
UserServicefromsrc/services/user-serviceand calluserService.getById(id).
The difference enforcement makes
Teams that enforce architecture at the merge gate report fundamentally different outcomes than teams that rely on documentation alone:
| Metric | Documentation only | autter enforcement |
|---|---|---|
| Boundary violations per sprint | 12-20 | 0-2 |
| Time spent on architecture review | ~8 hrs/week | ~1 hr/week |
| Months before major refactor needed | 6-9 months | 18+ months |
| New developer time to compliance | 4-6 weeks | Immediate |
The most significant impact is on AI-generated code. AI assistants have no awareness of your architectural decisions. Without enforcement at the merge gate, they will violate your boundaries in every PR — not out of malice, but out of ignorance.
Getting started
# Generate an initial architecture config from your codebase
npx autter architecture init
# Scan existing code for boundary violations
npx autter architecture check --report
# Enable enforcement on PRs
npx autter architecture enforceautter can generate an initial boundary configuration by analysing your existing import graph — showing you where boundaries already exist naturally and where they're being violated. Start with what you have, tighten as you go.
