Enforcing Architecture Decisions at the Merge Gate

Enforcing Architecture Decisions at the Merge Gate

Sagnik

Founder, autter.dev

3 min read

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:

RuleWhat it prevents
No circular dependenciesModule A imports B, B imports A
Layer directionPresentation → Business → Data, never reversed
Package isolationFeature packages don't cross-import
Shared kernel restrictionOnly 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/client in 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 UserService from src/services/user-service and call userService.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:

MetricDocumentation onlyautter enforcement
Boundary violations per sprint12-200-2
Time spent on architecture review~8 hrs/week~1 hr/week
Months before major refactor needed6-9 months18+ months
New developer time to compliance4-6 weeksImmediate

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 enforce

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

14-day free trial

Ship with confidence,
starting today

autter is the merge gate built for the AI coding era. Try it free for 14 days — no credit card, no commitment, full access to every feature.

  • AI-powered code reviews on every PR
  • 40+ linters & custom checks
  • No credit card required

Stop shipping code you can't fully trust

autter is the merge gate built for the AI coding era. It catches what linters miss, flags what CI ignores, and gives your team the confidence to ship faster without the 2am surprises.

50,000+

Pull requests analysed every week, catching issues that passed CI but would have failed in production.

73%

Fewer production incidents from AI-generated code after teams adopt the autter merge gate.

<90s

Average time to first review. autter analyses your PR before a human reviewer even opens it.

Capt. Patch

Capt. Autter Patch

Online now

I've seen a lot of codebases. Most teams find out they needed Autter after a bad deploy. What does your PR review process look like right now?

Powered by Autter AI