Software Systems Research

Architecture Governance for AI Code Generation

A practical guide for engineers and PMs who want AI coding agents to move fast without breaking architecture decisions, module boundaries, ownership, and long-term system quality.

20 min read Updated Jun 26, 2026

TL;DR

  • AI code generation makes architectural drift faster because agents can create many small, reasonable-looking changes that collectively violate system decisions.
  • The answer is not a bigger architecture document. The answer is architecture governance: a small set of living design artifacts, repo instructions, executable boundary checks, and PR gates.
  • Start by making the architecture explicit: system context, containers, components, ownership, allowed dependencies, and ADRs.
  • Then make it executable: lint rules, ArchUnit tests, module-boundary rules, API contract tests, dependency policies, security checks, and CI gates.
  • Give AI agents a “design packet” before generation: product intent, target boundary, accepted ADRs, forbidden imports, quality attributes, and validation commands.
  • PMs should care because this protects delivery predictability. Engineers should care because it keeps local productivity from becoming system-level entropy.
  • The practical goal is not to stop AI from changing architecture. The goal is to make architecture changes intentional, reviewed, traceable, and reversible.

AI coding agents are wonderful at local motion. They can add a route, write a migration, update tests, rename a component, and wire a dependency before a human has finished opening the right tabs.

That is also the risk.

Most architecture failures do not start with one dramatic decision. They start with small violations:

  • one feature imports from the wrong layer
  • one domain rule moves into a controller
  • one “temporary” integration bypasses the platform API
  • one service writes to another service’s database
  • one agent adds a duplicate abstraction because it did not know the original one existed
  • one PR passes tests while weakening a decision nobody wrote down

AI accelerates those moves. If the architecture only lives in people’s heads, the agent cannot respect it. If governance only happens in a meeting, the agent will outrun it.

This article focuses on architecture boundaries and decision traceability. For security gates, model selection, and approval workflows around agentic coding, see Secure Agentic Code Generation: A Practical Process. For orchestrating agents inside a delivery loop, see Senior Engineer Agent: Orchestrating Subagents with Loop Engineering.

So we need a different shape:

Architecture intent
  |
  v
Decision records + boundary maps + repo instructions
  |
  v
AI generation inside known boundaries
  |
  v
Executable fitness functions in CI
  |
  v
Human review for exceptions and new decisions

That is architecture governance for AI code generation.

What You Will Learn Here

  • How to design an application so future AI-generated code has clear boundaries.
  • Which architecture artifacts are useful to humans and AI agents.
  • How ADRs, C4 diagrams, module ownership, and fitness functions work together.
  • How to create a design packet for AI coding tasks.
  • How to turn architecture decisions into automated checks.
  • How PMs can participate without turning architecture into bureaucracy.
  • Where this approach still has gaps and what to improve next.

The Research Signal

The research and current tooling point in the same direction: architecture governance has to become more explicit and more executable.

Michael Nygard’s original ADR guidance argued for short, versioned records that capture significant decisions, their context, and their consequences. That is even more important with AI agents because future code authors may not share the team’s memory.

The ADR community defines an architecture decision as a justified design choice that affects a functional or non-functional requirement, and an ADR as the record of that choice and rationale. That gives us a durable unit of intent.

The C4 model gives teams a practical way to map systems at different levels: system context, containers, components, and code. This matters because AI agents need boundaries at the right level of detail. A product feature usually needs container and component context. A refactor may need package- or module-level dependency rules enforced by static analysis.

Fitness functions, popularized in Building Evolutionary Architectures (Ford, Parsons, and Kua, 2017), turn architecture goals into measurable signals. AWS describes them as measurable values that help guide architecture evolution while preserving team autonomy. That is exactly the balance we need for AI-assisted delivery: agents can move quickly, but the system keeps measuring whether the important properties still hold.

As of June 2026, major AI coding tools support persistent repository instructions, but they load them differently:

ToolInstruction filesHow they combine
OpenAI CodexAGENTS.md, nested AGENTS.override.mdConcatenates from repo root to cwd; later files override earlier ones (32 KiB default cap)
GitHub CopilotAGENTS.md, .github/copilot-instructions.md, .github/instructions/*.instructions.mdNearest AGENTS.md wins; path-specific files need applyTo frontmatter
VS Code agents.github/copilot-instructions.md, custom instructions, optional nested AGENTS.mdMultiple sources may apply; nested AGENTS.md is experimental and opt-in

Path-specific Copilot instructions on GitHub.com currently apply to Copilot cloud agent and Copilot code review, not every Copilot surface. VS Code custom instructions apply to chat and agent workflows, not inline completions.

The inference is clear:

Architecture governance for AI code generation should be both readable and runnable.

Readable artifacts help humans and agents understand what should happen. Runnable artifacts stop accidental drift before merge.

Architecture Governance, Not Governance Theater

Architecture governance is the control plane around AI code generation.

It does not mean every change needs an architecture committee. It means the important decisions have a place to live, a way to be found, and a way to be checked.

              Architecture Governance

  +----------------+      +--------------------+
  | Design Intent  | ---> | Decision Records   |
  | goals, risks   |      | ADRs, tradeoffs    |
  +----------------+      +--------------------+
          |                         |
          v                         v
  +----------------+      +--------------------+
  | Boundary Map   | ---> | Agent Instructions |
  | ownership, deps|      | AGENTS.md, prompts |
  +----------------+      +--------------------+
          |                         |
          v                         v
  +----------------+      +--------------------+
  | Fitness Checks | ---> | Review Gates       |
  | CI, tests      |      | PRs, exceptions    |
  +----------------+      +--------------------+

A good architecture governance system has five properties:

  • Explicit: the boundary is written down.
  • Local: the rule appears near the code it protects.
  • Executable: important rules are checked automatically.
  • Auditable: exceptions and decisions are visible in Git history.
  • Small: the system protects a few high-value decisions instead of pretending to control everything.

The last point matters. If every preference becomes a gate, engineers route around the system. If only the sharp edges become gates, the system earns trust.

Start With Application Design Boundaries

Before asking AI to generate code, decide what kind of application you are building. This does not need to be a giant design phase. It can be a one-page architecture brief.

For a typical SaaS application, a practical starting point looks like this:

Browser
  |
  v
Web/API Edge
  |
  v
Application Use Cases
  |
  v
Domain Model
  |
  v
Ports
  |
  v
Adapters: database, queue, email, payments, AI tools

That diagram is hexagonal (ports-and-adapters) layering. It is not the same thing as domain ownership, which governs which team or module may call which adapter. You usually need both: layering rules inside a module, and ownership rules across modules.

The first layering rule:

Dependencies point inward. Infrastructure adapts to the domain. The domain does not know the framework, database, queue, or LLM provider.

For PMs, this boundary means the team can change payment providers, model providers, or database implementation without rewriting the product concept.

For engineers, it means generated code has obvious homes:

New codeExpected place
HTTP request parsingEdge
User workflowApplication use case
Business invariantDomain
SQL queryDatabase adapter
Queue publishingMessaging adapter
AI tool callAI adapter
Permission decisionAuthorization policy layer (centralized policy objects or services, not ad hoc route checks)

When the agent proposes a change, review the location first. Wrong location is often the first sign of architecture drift.

Create a Decision Stack

AI agents need context, but context must be structured. A pile of documents becomes noise. A decision stack gives the agent and reviewers a clear order of authority.

1. Product goal
2. Quality attributes
3. Architecture principles
4. Accepted ADRs
5. Boundary map
6. Local code conventions
7. Current task

Each layer answers a different question:

LayerQuestion
Product goalWhy does this capability exist?
Quality attributesWhich tradeoffs matter most?
Architecture principlesWhat should remain true across the system?
ADRsWhich decisions have already been accepted?
Boundary mapWhere is code allowed to depend?
Local conventionsHow is this repo organized?
Current taskWhat should change now?

This is useful because agents often optimize for the most recent instruction. The decision stack gives the latest task a frame instead of letting it override everything.

A Minimal ADR Template

Keep ADRs short enough that people will actually write them.

# ADR-004: Keep payment orchestration in the billing domain

Status: Accepted
Date: 2026-06-11

## Context

Checkout, subscription renewal, refunds, and invoice retries all need the same
payment-state rules. Recent features have started duplicating payment workflow
logic in API handlers and background jobs.

## Decision

Payment orchestration will live in the billing domain application layer.
API handlers and jobs may call billing use cases, but they must not call the
payment provider adapter directly.

## Consequences

- Billing owns retry, idempotency, and payment-state transitions.
- Feature teams can still add payment-triggering flows through billing use cases.
- Direct imports from `adapters/payments` outside billing are architectural violations.

## Enforcement

- Boundary test: only `billing/**` may import `adapters/payments/**`.
- PR checklist: payment changes must link this ADR.

Notice the final section: enforcement. Traditional ADRs often stop at consequences. AI-era ADRs should also say how the decision is protected, and the same PR that accepts an ADR should update the matching ArchUnit rule, Nx tag constraint, or custom checker. An ADR without a matching automated rule is documentation, not governance.

Give AI a Design Packet

Do not start with “build the thing.” Start with a design packet.

Design packet
-------------
Goal: Add invoice retry settings for account admins.

Target boundary:
- UI: account settings
- API: billing routes
- Use case: billing/updateRetryPolicy
- Domain: billing retry policy

Accepted decisions:
- ADR-004: payment orchestration stays in billing
- ADR-007: authorization is checked through policy service

Forbidden moves:
- Do not call payment provider adapters from routes.
- Do not duplicate retry state in account profile tables.
- Do not add a new role model.

Validation:
- npm run lint
- npm test
- npm run test:architecture   # example script name; wire to your checker

This packet is friendly for engineers and PMs. PMs can validate the goal and risk. Engineers can validate the boundaries. AI agents can translate the packet into code without guessing the architecture.

For teams already using spec-driven workflows, the design packet can sit alongside spec-kit artifacts. See How to Reverse-Engineer Existing Features into Spec-Kit Artifacts.

Put Agent Instructions in the Repo

Repo instructions are the bridge between architecture and AI tools. They should be short, specific, and testable.

Example AGENTS.md:

# AGENTS.md

## Architecture

- Keep domain code independent from frameworks, databases, queues, and LLM SDKs.
- Application use cases may depend on domain code and ports.
- Adapters implement ports. Domain and use cases must not import adapters directly.
- New cross-boundary dependency rules require an ADR.

## AI Coding Workflow

- Before editing files, identify the target boundary and relevant ADRs.
- If a task requires crossing a forbidden dependency, stop and propose an ADR.
- Prefer small PRs that preserve existing module ownership.
- Run `npm run lint`, `npm test`, and `npm run test:architecture` when relevant.

## Forbidden Patterns

- Do not bypass the authorization policy layer.
- Do not write directly to another domain's tables.
- Do not introduce a second implementation of an existing domain service.

This is not magic. Instructions can be missed, misunderstood, truncated, or overridden by conflicting context. Codex stops loading after 32 KiB by default; Copilot picks the nearest AGENTS.md; VS Code may merge multiple instruction sources with no guaranteed precedence. That is why the same rules need executable checks.

Turn Boundaries Into Code

Architecture decisions become durable when they can fail a build. Teams usually need two kinds of checks: layering rules inside a module and ownership rules across modules.

For Java teams, ArchUnit can enforce layers, slices, onion architecture, and dependency rules. The onionArchitecture() API validates dependency direction inside the analyzed package. It does not, by itself, enforce cross-package ownership like ADR-004.

Layering inside billing:

import static com.tngtech.archunit.library.Architectures.onionArchitecture;

import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;

@AnalyzeClasses(
    packages = "com.acme.billing",
    importOptions = ImportOption.DoNotIncludeTests.class
)
class BillingLayeringTest {

    @ArchTest
    static final ArchRule billing_keeps_domain_independent =
        onionArchitecture()
            .domainModels("com.acme.billing.domain.model..")
            .domainServices("com.acme.billing.domain.service..")
            .applicationServices("com.acme.billing.application..")
            .adapter("persistence", "com.acme.billing.adapter.persistence..")
            .adapter("payments", "com.acme.billing.adapter.payments..")
            .adapter("api", "com.acme.billing.adapter.api..");
}

Ownership for ADR-004 needs a separate custom rule:

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

// ...

@ArchTest
static final ArchRule only_billing_may_use_payment_adapter =
    noClasses()
        .that().resideOutsideOf("..billing..")
        .should().dependOnClassesThat()
        .resideInAnyPackage("..adapter.payments..");

For TypeScript monorepos, Nx enforces project graph boundaries with tags and dependency constraints. The rule is an ESLint check, and each Nx project must declare tags in project.json or package.json before constraints take effect.

// eslint.config.mjs (or .eslintrc.json) — excerpt
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "allow": [],
        "depConstraints": [
          {
            "sourceTag": "type:ui",
            "onlyDependOnLibsWithTags": ["type:ui", "type:api-client", "type:shared"]
          },
          {
            "sourceTag": "type:domain",
            "onlyDependOnLibsWithTags": ["type:domain", "type:shared"]
          },
          {
            "sourceTag": "scope:billing",
            "onlyDependOnLibsWithTags": ["scope:billing", "scope:shared"]
          }
        ]
      }
    ]
  }
}

For any stack, you can start with a team-defined architecture manifest and a custom checker. The YAML below is illustrative, not a standard format any tool reads out of the box:

# architecture-boundaries.yaml — team-defined schema
domains:
  billing:
    owns:
      - src/billing/**
      - db/migrations/*billing*
    may_depend_on:
      - shared
      - identity
    forbidden_imports:
      - src/adapters/payments/** from outside src/billing/**
      - src/admin/** from src/billing/domain/**

quality_gates:
  require_adr_when:
    - adding a new database
    - adding a new service
    - crossing domain ownership
    - bypassing a platform API

Your team implements tools/architecture/check-boundaries.ts (or equivalent) to read that manifest. Then wire the checker into CI:

name: Architecture

on:
  pull_request:

jobs:
  architecture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run lint
      - run: npm test
      - run: npm run test:architecture   # maps to your ArchUnit/Nx/custom checker
      - run: npm run check:boundaries    # optional: custom manifest checker

Polyglot repos often need multiple checkers: ArchUnit for JVM services, Nx for TypeScript apps, and a custom graph tool for everything else.

The first version does not need to catch everything. It needs to catch the top two or three violations that would make the architecture meaningfully worse.

Use Fitness Functions as Guardrails

A fitness function is an objective check that tells you whether an architecture characteristic is still healthy. In practice, teams start with automated CI checks and grow toward observability metrics, dashboards, and manual review signals over time.

Phase 1: automate in CI first

Architecture riskFitness function
Domain depends on infrastructureImport rule or ArchUnit test
Circular module dependenciesCycle detection
API contracts driftContract tests
Security bypassPolicy-layer test
Cloud driftIaC policy check

Phase 2+: semi-automated or bespoke

Architecture riskFitness function
New service without ownershipService catalog check (needs catalog integration)
Database ownership violationMigration/table ownership check (custom tooling)
Duplicate abstractionsSearch or static analysis rule (heuristic)
Performance regressionBudgeted benchmark

The governance trick is to start small:

Week 1: protect dependency direction
Week 2: protect authorization path
Week 3: protect API contracts
Week 4: add dashboard and exception workflow

Three meaningful checks beat thirty decorative checks.

Define an Exception Path

Sometimes the agent is right to challenge the architecture. Maybe the old boundary is wrong. Maybe the product has changed. Maybe the existing abstraction is carrying too much weight.

Governance should not block those discoveries. It should require the right artifact.

AI or engineer finds boundary pressure
  |
  v
Does current ADR allow it?
  | yes
  v
Implement within boundary

  | no
  v
Write proposal ADR
  |
  v
Review tradeoff with owner + PM
  |
  v
Accept, reject, or timebox exception
  |
  v
Update ADR, checks, and repo instructions in the same PR

When an exception is accepted, update ArchUnit rules, Nx tag constraints, or custom checkers in the same change set as the ADR. Otherwise the exception becomes invisible technical debt.

The key rule:

A boundary can change, but it must change as a decision, not as a side effect.

That sentence is the heart of the whole article.

A Practical PR Review Gate

Every AI-generated PR should answer a few architecture questions.

## Architecture Review

- Target boundary:
- Relevant ADRs:
- New dependencies:
- Cross-domain calls:
- Data ownership touched:
- Authorization path:
- Fitness checks run:
- Exceptions requested:

For small UI-only changes, this takes a minute. For risky changes, it forces the right conversation before merge.

A PM does not need to inspect every import. But a PM can read the target boundary, relevant ADRs, and exceptions requested. That creates shared visibility into why a feature is easy, risky, or blocked.

Where AI Should Be Allowed to Move Fast

Not every change needs the same governance weight.

Low risk
-------
- UI copy
- isolated component styling
- test fixtures
- small bug fixes inside one module

Medium risk
-----------
- new API route
- new database query
- new background job
- shared component behavior

High risk
---------
- new service or database
- auth or authorization change
- payment, billing, identity, privacy, or security logic
- cross-domain dependency
- event schema or public API change
- migration that changes ownership of data

Match the workflow to the risk:

RiskAI workflow
LowAgent can implement with normal review
MediumAgent needs design packet and checks
HighAgent drafts plan first, human approves design, PR links ADR

This keeps governance from slowing harmless work while protecting the decisions that matter.

The PM and Engineering Contract

Architecture governance works better when PMs are included early.

PMs should not have to approve package imports. But PMs should understand:

  • which product capabilities are easy because they fit the current architecture
  • which capabilities are expensive because they cross ownership boundaries
  • which decisions are temporary
  • which quality attributes are non-negotiable
  • which exceptions create future cleanup work

A useful PM-facing translation:

Engineering phrasePM translation
Boundary violationThis feature is crossing product ownership or system responsibility
ADR requiredWe are changing a decision that affects future delivery
Fitness check failedThe system caught a drift risk before merge
Exception acceptedWe are taking a known shortcut with an owner and expiry date
Architecture runwayWe need enabling work so future features stay cheap

This is how governance becomes delivery clarity instead of hidden engineering friction.

Suggested Repository Layout

One practical layout:

repo/
  AGENTS.md
  docs/
    architecture/
      context.md
      containers.md
      boundaries.md
      fitness-functions.md
      adr/
        0001-use-modular-monolith.md
        0002-authorization-policy-layer.md
        0003-domain-ownership.md
  .github/
    copilot-instructions.md
    pull_request_template.md
    instructions/
      architecture.instructions.md
  tools/
    architecture/
      check-boundaries.ts
  src/
    billing/
    identity/
    shared/

Path-specific Copilot instructions need frontmatter so GitHub knows which files they apply to:

---
applyTo: "src/billing/**,src/identity/**"
---

# Architecture instructions for domain modules

- Read ADR-004 before touching payment adapters.
- Run `npm run test:architecture` before opening a PR.

The layout has three audiences:

  • Humans read docs/architecture.
  • AI agents read AGENTS.md, .github/copilot-instructions.md, and .github/instructions.
  • CI runs tools/architecture and your test:architecture script.

The same decision appears in three forms: explanation, instruction, and enforcement.

A 30-Day Rollout Plan

You do not need to redesign the whole engineering org. Start with one repo and one product area.

Days 1-3
Map the current architecture in one page.
Name the main domains, owners, data stores, and external systems.

Days 4-7
Write the first five ADRs.
Capture decisions that already exist but are only remembered by senior engineers.

Days 8-14
Create AGENTS.md and a PR architecture checklist.
Make AI agents state the target boundary before implementation.

Days 15-21
Add two executable checks.
Start with dependency direction and authorization path.

Days 22-30
Review exceptions.
Update docs, instructions, and checks based on real PR friction.

The first milestone is not perfect governance. It is one repo where architecture decisions are no longer invisible.

Existing Gaps

This approach is useful, but it has limits.

  • Instruction files are advisory. AI tools can miss or misapply instructions, especially when context is long or conflicting. Codex truncates at 32 KiB by default; Copilot uses nearest-wins discovery; VS Code nested AGENTS.md is experimental.
  • Checks cover known risks. Fitness functions are excellent for known boundaries, but weak for discovering brand-new design problems.
  • Legacy systems need mapping first. If ownership and dependencies are already unclear, AI governance starts with archaeology. See Integrate GitHub Spec-Kit into Existing Repos for one way to reverse-engineer structure.
  • False positives can hurt adoption. A noisy architecture gate becomes a tax. Keep rules few, clear, and fixable.
  • PM visibility is often missing. If ADRs are too technical, product leaders will not understand the delivery consequences.
  • Architecture exceptions need expiry. Without owners and dates, exceptions become the new architecture.
  • Stack coverage is uneven. ArchUnit is JVM-only; Nx boundaries apply to project graph dependencies, not every filesystem path; polyglot systems need multiple checkers.

The biggest gap is semantic judgment. A tool can detect an import violation. It cannot always tell whether a new business capability deserves a new boundary. That remains human architecture work.

Final Model

The simplest mental model is this:

AI can generate code.
Architecture governance tells it where code belongs.
Fitness functions prove whether the rules still hold.
ADRs decide when the rules should change.

That is the balance. Let AI make local work faster. Make the architecture strong enough that local speed does not quietly become system failure.

Sources