How to Stop AI Generating Tightly Coupled Code
Coding agents take shortcuts through an architecture for the same reason people do, because the direct path still passes the task. Prose rules (‘respect the layers’) improve first drafts but cannot guarantee the result. We rely on three deterministic controls working together. ArchUnitNET tests make forbidden dependencies a failing build, banned-API rules remove the escape hatches, and a PostToolUse hook runs both after every edit so the agent corrects itself in the turn the violation happens.
- Write the layer rules as executable tests rather than documentation.
- Ban the bypass APIs at compile time so the shortcut is not available.
- Feed violations back at edit time through hooks, because by the time CI catches them they are slower and more expensive to fix.
Our harness engineering guide sets out the full control framework.
Teams adopting coding agents for serious .NET work tend to raise the same architectural worry, often in the same words: how do we stop the AI generating tightly coupled code? They have a target architecture, typically a clean layering of web, application services, domain, and data access. They also have a reasonable fear that fast generation will erode it one convenient shortcut at a time.
That fear is justified when the only protection is written guidance. In our experience it goes away once the architecture is enforced by checks that fail the build.
Why agents cut through layers
An agent asked to ‘add an endpoint that returns overdue invoices’ can take one of two paths. The architectural path extends the repository, adds a service method, and calls it from a thin controller. The direct path puts a database query in the controller. Both pass the task, but the second takes fewer steps, and the public code models are trained on contains millions of examples of it.
Rules files help. A CLAUDE.md that names the layers and forbids cross-layer references measurably improves first drafts, and every project should have one. Our Claude Code for .NET developers guide covers what to put in AGENTS.md and CLAUDE.md, and our .NET configuration guide shows the wider setup.
A rules file is still only context, though, and context degrades under pressure from long sessions, competing instructions, and unusual tasks. Written guidance shapes the draft without guaranteeing it, so the guarantee has to come from checks the model cannot argue with.
Control one: layer rules as executable tests
ArchUnitNET turns the architecture diagram into unit tests. The rules that protect a typical layered .NET solution fit on one screen:
public class ArchitectureTests
{
private static readonly Architecture Architecture =
new ArchLoader()
.LoadAssemblies(
typeof(Web.Program).Assembly,
typeof(Application.OrderService).Assembly,
typeof(Domain.Order).Assembly,
typeof(DataAccess.OrderRepository).Assembly)
.Build();
private readonly IObjectProvider<IType> _web =
Types().That().ResideInAssembly("MyApp.Web").As("Web layer");
private readonly IObjectProvider<IType> _dataAccess =
Types().That().ResideInAssembly("MyApp.DataAccess").As("Data access layer");
private readonly IObjectProvider<IType> _domain =
Types().That().ResideInAssembly("MyApp.Domain").As("Domain layer");
[Fact]
public void Web_Must_Not_Reference_DataAccess() =>
Types().That().Are(_web)
.Should().NotDependOnAny(_dataAccess)
.Because("controllers and pages go through application services")
.Check(Architecture);
[Fact]
public void Domain_Must_Not_Reference_Any_Outer_Layer() =>
Types().That().Are(_domain)
.Should().NotDependOnAny(_dataAccess)
.AndShould().NotDependOnAny(_web)
.Because("the domain is the innermost layer")
.Check(Architecture);
}The Because strings are more useful to an agent than to a person. They appear in the failure output, so the agent reads why the rule exists as well as the fact that it broke, and that is what it needs to choose the right fix. We find agent-readable error messages are one of the most effective parts of a harness, and our maintainability sensors guide covers them in more depth.
Control two: remove the escape hatches
Layer tests catch references between your own projects. A harder violation to spot reaches around the layers through a framework API. A raw SqlConnection in a controller references none of your forbidden assemblies, only Microsoft’s, so the layer tests above still pass.
Banned-API rules close that route. The BannedApiAnalyzers package, with a BannedSymbols.txt scoped to the projects outside data access, flags the shortcut at compile time:
T:Microsoft.Data.SqlClient.SqlConnection; Data access goes through the repository layer
T:Microsoft.Data.SqlClient.SqlCommand; Data access goes through the repository layerWith TreatWarningsAsErrors on, that flag becomes a build failure the model cannot generate its way past. The same mechanism carries security rules, such as banning raw SQL execution APIs, and our secure AI-generated code guide works through them.
Control three: feedback at edit time, not review time
The two controls above run wherever the build runs, so the timing of that build decides how useful they are. We use a PostToolUse hook that builds and runs the architecture tests immediately after every file edit. The agent sees a violation seconds after creating it, while its intent is still in context, and fixes it in the same turn.
Without the hook, the violation survives the session and lands in a pull request. Either a reviewer catches it, which is expensive, or nobody does and the coupling stays. With hook-driven feedback, the agent corrects these violations itself instead of leaving them for review. The hook JSON is in the .NET configuration guide, and the same setup also gives the agent formatting and analyser feedback at no extra cost.
Putting the three controls together
We state the architecture in CLAUDE.md so the first draft is usually right. ArchUnitNET and banned APIs then stop a wrong draft from building, and hooks feed the failures back so the agent fixes its own violations before a reviewer sees them.
The same approach works for other rules you care about, not only coupling: written guidance for the first draft, deterministic checks to enforce the rule, and fast feedback to keep fixes cheap. It is the core of harness engineering for coding agents. It is also how modernisation programmes keep their target architecture intact while agents work at speed, including the enforced layering in our WinForms to web rebuild guide.
If you are setting up an agent harness for a .NET team, book a consultation and we will review your architecture rules with you.