Software architecture for AI-written code: boundaries agents can't break
An AI agent sees a few files at a time. Any architecture that lives only in your senior engineers' heads, it will break — politely, confidently and in code that passes review. The fix is to make the architecture something the build can check.
How agents erode architecture
Agents optimise for the task in front of them. Across hundreds of changes that adds up to familiar damage:
- imports that reach into another module's internals because it was the shortest path;
- a fourth copy of a helper that already exists three folders away;
- business rules in UI components and SQL in request handlers;
- the nearest pattern copied — including the legacy one you are migrating away from.
None of it is wrong line by line. It is wrong for the system, and it only shows up when you look at the system.
Make boundaries machine-checkable
Write the dependency rules down as code and run them in CI on every change, human or agent. Each ecosystem has a tool: dependency-cruiser for TypeScript, import-linter for Python, ArchUnit for Java and Kotlin, analyzer rules for Dart.
// .dependency-cruiser.cjs
module.exports = {
forbidden: [
{
name: "no-module-internals",
comment: "Import another module only through its public index.ts",
severity: "error",
from: { path: "^src/modules/([^/]+)/" },
to: {
path: "^src/modules/[^/]+/(?!index\\.ts$)",
pathNot: "^src/modules/$1/",
},
},
{
name: "ui-never-touches-db",
severity: "error",
from: { path: "^src/app/" },
to: { path: "^src/platform/db/" },
},
],
};
A failing rule is the best feedback an agent can get: specific, immediate and impossible to argue with.
Contracts over conventions
- Typed interfaces between modules, not shared mutable objects.
- Schema-first APIs — OpenAPI, protobuf or GraphQL — with generated clients, so the contract is a file, not a habit.
- Contracts behind review. A
CODEOWNERSrule on schemas and database migrations means an agent can implement against a contract but can't quietly change it.
Small, deep modules
Agents do their best work when a task fits inside one module with a narrow public surface. Give each module a short README: what it is for, its public API, the invariants it protects and what it must never depend on. That is also the context an agent loads before it starts.
A map agents actually read
- An architecture section in
AGENTS.md: the modules, which may depend on which, and where new code goes. It is one part of a wider AI agent harness. - Architecture decision records in
docs/adr/: one short file per decision, with the reason. Agents (and new engineers) follow rules better when they can read why.
Review the architecture, not just the lines
Line-by-line review doesn't catch architectural drift. When I audit AI-written code, I look at the system level first: how the dependency graph changed, which public APIs appeared, who now writes to which tables, and where logic got duplicated. Then come the rules and checks that stop the same drift from coming back.
A quick checklist
- Are module boundaries enforced by a check that fails the build?
- Are API schemas and migrations owned by humans in
CODEOWNERS? - Does every module have a README with its public API and invariants?
- Does
AGENTS.mdsay where new code goes?