Skip to content
aicoding-agentsarchitecturedotnetclaude-code

How to Stop AI Generating Tightly Coupled Code

Matt Hammond 6 min read
Structural steel framework of a building against the sky, an enforced architecture every part must fit

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:

C#
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:

Text
T:Microsoft.Data.SqlClient.SqlConnection; Data access goes through the repository layer
T:Microsoft.Data.SqlClient.SqlCommand; Data access goes through the repository layer

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

Frequently asked questions

Why do coding agents produce tightly coupled code?
The shortest path to a passing task often cuts through a layer: calling the database directly from a controller takes fewer steps than going through a service and repository. Models reproduce common patterns, and public code is full of shortcuts. Without enforcement, an agent under context pressure will sometimes take the shortcut even when your rules file says not to.
Are prompt rules enough to protect an architecture?
No. Rules in CLAUDE.md or AGENTS.md improve the first draft and you should have them, but prose guidance cannot guarantee outcomes. The guarantee comes from deterministic checks, such as architecture tests and analysers that fail the build as soon as a dependency crosses a forbidden boundary. The violation then cannot merge, whatever the model intended.
What is ArchUnitNET?
An architecture testing library for .NET that lets you write executable rules about dependencies between assemblies, namespaces, and types: for example, that the web layer never references data access directly. The rules run as ordinary unit tests, so they fail builds, run in CI, and give a coding agent immediate, readable feedback when it breaks a boundary.
How do hooks make architecture enforcement faster?
A PostToolUse hook runs the build and the architecture tests immediately after every file edit, so the agent sees a violation seconds after creating it and fixes it in the same turn. Without the hook, violations accumulate until a later build or CI run, when they are more expensive to unwind and the agent has lost the context.

Ready to transform your software?

Let's talk about your project. Contact us for a free consultation and see how we can deliver a business-critical solution at startup speed.