Introduction
AI coding assistants can generate a surprising amount of code in a short time. A developer can describe a feature, ask an AI agent to implement it, and receive changes across controllers, services, repositories, tests, and configuration files.
That speed is useful, but it introduces a new engineering problem.
A pull request can compile, pass unit tests, and still violate the architecture of the application.
For example, a .NET application may follow this structure:
API
|
v
Application
|
v
Domain
|
v
Infrastructure
But an AI-generated change might accidentally create:
API
|
+------> Infrastructure
|
+------> Database
The code may work.
The tests may pass.
The architecture is still wrong.
This is why architecture gates are becoming increasingly useful for AI-assisted development.
Instead of asking only:
"Does the code work?"
the pull-request pipeline should also ask:
"Does the code follow the architecture?"
What Is an Architecture Gate?
An architecture gate is an automated validation step that checks whether a code change follows predefined architectural rules.
A traditional CI pipeline might look like:
Pull Request
|
v
Build
|
v
Unit Tests
|
v
Integration Tests
|
v
Merge
An architecture-aware pipeline adds another layer:
Pull Request
|
v
Build
|
v
Tests
|
v
Architecture Gate
|
v
Security / Quality Checks
|
v
Human Review
|
v
Merge
This becomes particularly useful when AI agents are generating or modifying large portions of the codebase.
Why AI-Generated Code Needs Architecture Checks
AI systems are very good at producing locally reasonable code.
The problem is that software architecture is a global concern.
Suppose the project has:
Domain
Application
Infrastructure
API
A developer may know that:
Domain
must not depend on:
Infrastructure
An AI agent may not always preserve that rule unless the repository provides enough context and the workflow enforces it.
This creates a useful distinction:
AI Instructions
|
v
"Please follow Clean Architecture."
versus:
Automated Rule
|
v
"Domain cannot reference Infrastructure."
The second is enforceable.
Architecture as an Executable Contract
Architecture documentation is useful, but documentation alone does not prevent violations.
Consider this rule:
The Domain layer must not depend on Infrastructure.
You can document it in:
README
Architecture Document
Developer Guide
But an automated architecture test can turn it into an executable rule.
Conceptually:
Architecture Rule
|
v
Automated Test
|
+---- Pass ---> Pull Request continues
|
+---- Fail ---> Pull Request blocked
This makes architecture part of the software delivery process.
Example .NET Architecture
Consider a typical ASP.NET Core solution:
src/
├── Store.Api/
├── Store.Application/
├── Store.Domain/
├── Store.Infrastructure/
└── Store.Contracts/
A reasonable dependency model might be:
Store.Api
|
v
Store.Application
|
v
Store.Domain
Store.Infrastructure
|
+----> Store.Application
|
+----> Store.Domain
The important restriction is:
Store.Domain
X
Store.Infrastructure
and potentially:
Store.Application
X
Store.Api
The exact dependency direction depends on the architecture, but it should be explicit.
Enforcing Architecture With NetArchTest
One practical approach for .NET applications is using an architecture-testing library such as NetArchTest.
The idea is simple: inspect assemblies and assert architectural rules.
For example:
[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
var result = Types
.InAssembly(typeof(Order).Assembly)
.ShouldNot()
.HaveDependencyOn("Store.Infrastructure")
.GetResult();
Assert.True(result.IsSuccessful);
}
The exact namespaces and assembly structure will vary by application.
The important concept is that architecture becomes testable.
Testing Layer Dependencies
Suppose the application uses these layers:
API
Application
Domain
Infrastructure
You can define rules such as:
Domain
-> No Infrastructure
Domain
-> No API
Application
-> No API
Infrastructure
-> Can depend on Application and Domain
A test suite can represent those rules.
For example:
[Fact]
public void Domain_Should_Not_Depend_On_Api()
{
var result = Types
.InAssembly(typeof(Order).Assembly)
.ShouldNot()
.HaveDependencyOn("Store.Api")
.GetResult();
Assert.True(result.IsSuccessful);
}
Now an accidental dependency can fail the pull request automatically.
Architecture Tests vs Unit Tests
These tests answer different questions.
| Test Type | Main Question |
|---|
| Unit test | Does this component behave correctly? |
| Integration test | Do components work together? |
| API test | Does the API behave correctly? |
| Architecture test | Does the code follow structural rules? |
| Security test | Does the code meet security expectations? |
A project should not rely on one category alone.
For example:
Unit Tests -> Correct behavior
Integration Tests -> Correct integration
Architecture Tests -> Correct structure
Security Checks -> Safer implementation
Together they provide stronger validation.
AI Agents and Dependency Direction
Imagine an AI agent is asked:
Add persistence for Customer preferences.
The agent creates:
CustomerPreferencesService
|
v
DbContext
inside the Application layer.
The code may compile.
But suppose the architecture requires persistence to remain inside Infrastructure.
An architecture gate can detect the dependency.
Without the gate:
AI Change
|
v
Build Pass
|
v
Tests Pass
|
v
Merge
With the gate:
AI Change
|
v
Build Pass
|
v
Tests Pass
|
v
Architecture Gate
|
X
Violation
The pull request stops before the architectural problem reaches the main branch.
Architecture Rules Should Be Specific
Avoid vague rules such as:
"Keep the architecture clean."
That is difficult to automate.
Instead define concrete rules:
Domain cannot reference Infrastructure.
Controllers cannot directly access DbContext.
Application handlers cannot depend on ASP.NET Core controllers.
Infrastructure implementations cannot be referenced by Domain.
API projects cannot contain database queries.
These rules can be tested.
Preventing Controllers From Becoming Business Logic
A common architecture rule is keeping controllers thin.
Bad example:
[HttpPost]
public async Task<IActionResult> CreateOrder(
CreateOrderRequest request)
{
var customer = await db.Customers
.FirstAsync(x => x.Id == request.CustomerId);
var order = new Order();
order.Total = request.Items.Sum(
x => x.Price * x.Quantity);
db.Orders.Add(order);
await db.SaveChangesAsync();
return Ok(order);
}
The controller is handling:
Database access
Business logic
Calculation
Persistence
HTTP response
A more structured design might be:
Controller
|
v
Application Service
|
v
Domain
|
v
Infrastructure
The controller becomes simpler:
[HttpPost]
public async Task<IActionResult> CreateOrder(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
var result = await service.CreateOrderAsync(
request,
cancellationToken);
return Ok(result);
}
An architecture gate can help prevent developers, including AI agents, from bypassing the intended layers.
Restricting Direct DbContext Usage
Suppose the architecture says:
Only Infrastructure can directly use AppDbContext.
An architecture test can search for dependencies on the database assembly.
Conceptually:
API
X
DbContext
Application
X
DbContext
Infrastructure
OK
DbContext
This prevents a common form of architectural drift.
Namespace Rules
Architecture does not only exist at the project level.
Namespaces can also express boundaries.
For example:
Store.Domain.Entities
Store.Domain.ValueObjects
Store.Domain.Services
A rule could prevent domain entities from depending on:
Microsoft.AspNetCore.*
Microsoft.EntityFrameworkCore.*
This helps keep the domain model independent from infrastructure frameworks.
Example Framework Dependency Test
A simplified test might look like:
[Fact]
public void Domain_Should_Not_Depend_On_EfCore()
{
var result = Types
.InAssembly(typeof(Order).Assembly)
.ShouldNot()
.HaveDependencyOn(
"Microsoft.EntityFrameworkCore")
.GetResult();
Assert.True(result.IsSuccessful);
}
Again, the correct rule depends on the project's architecture.
The purpose is not to ban frameworks universally.
The purpose is to enforce deliberate boundaries.
Architecture Gates in CI
An architecture test is most useful when it runs automatically.
A .NET CI pipeline can conceptually look like:
Pull Request
|
v
Restore
|
v
Build
|
v
Unit Tests
|
v
Integration Tests
|
v
Architecture Tests
|
v
Security Checks
|
v
Review
|
v
Merge
The architecture tests should run on every relevant pull request.
For example:
dotnet restore
dotnet build --no-restore
dotnet test --no-build
If architecture tests are included in the test project, they become part of the normal validation process.
Making the Gate Fail Clearly
A failed architecture test should explain what went wrong.
Bad output:
Test Failed.
Better:
Architecture violation:
Store.Domain has a dependency on
Store.Infrastructure.
Expected:
Domain -> Infrastructure = prohibited.
Clear failure messages reduce debugging time.
Architecture Gates and Pull Requests
A useful pull-request workflow is:
Developer or AI Agent
|
v
Code Changes
|
v
Pull Request
|
+--> Build
|
+--> Tests
|
+--> Architecture
|
+--> Security
|
v
Human Review
The human reviewer can then focus more on business correctness and design decisions instead of manually checking every dependency relationship.
AI Instructions Still Have Value
Architecture gates should not replace repository instructions.
They serve different purposes.
Instructions can explain:
Use the application service pattern.
Keep controllers thin.
Use domain services for business rules.
Architecture tests enforce:
Controller cannot reference Infrastructure.
A useful model is:
Instructions
+
Tests
+
CI Policies
=
Stronger AI-Assisted Development
The AI receives guidance.
The automated system verifies the result.
Architecture Gates for Naming
Architecture tests can also enforce naming conventions.
For example:
Commands must end with Command.
Handlers must end with Handler.
Controllers must end with Controller.
Repositories must end with Repository.
A test could conceptually enforce:
[Fact]
public void Handlers_Should_End_With_Handler()
{
var result = Types
.InAssembly(typeof(CreateOrderHandler).Assembly)
.That()
.ResideInNamespace("Store.Application.Handlers")
.Should()
.HaveNameEndingWith("Handler")
.GetResult();
Assert.True(result.IsSuccessful);
}
This can be particularly useful when AI-generated code introduces inconsistent naming.
Architecture Gates for Interfaces
Another useful rule is ensuring implementations follow the expected abstraction.
For example:
IOrderRepository
^
|
OrderRepository
An automated test can verify that classes in the repository namespace implement the expected interfaces.
This can catch incomplete AI-generated changes.
Architecture Gates for Dependency Injection
Suppose the project requires:
Application services
|
v
Interfaces
|
v
Infrastructure implementations
A generated class that directly constructs a dependency is a potential architectural problem.
For example:
var repository = new SqlOrderRepository();
could bypass dependency injection.
A better pattern is:
public class OrderService(
IOrderRepository repository)
{
private readonly IOrderRepository repository = repository;
}
Architecture rules can detect some of these patterns, while code-quality analyzers can handle others.
Architecture Gates for Project References
Project references provide another strong boundary.
Suppose:
Store.Domain.csproj
should never reference:
Store.Infrastructure.csproj
This can be checked before runtime.
For example, the project dependency graph should remain:
Store.Api
|
+--> Store.Application
|
+--> Store.Infrastructure
Store.Application
|
+--> Store.Domain
Store.Infrastructure
|
+--> Store.Application
|
+--> Store.Domain
A dependency graph makes architectural violations easier to understand.
Architecture Drift
Architecture tends to degrade gradually.
One developer adds a shortcut.
Another copies it.
An AI agent learns the existing pattern.
Soon the shortcut becomes normal.
The progression can look like:
Clean Architecture
|
v
One Exception
|
v
More Exceptions
|
v
Repeated Pattern
|
v
Architecture Drift
Architecture gates interrupt this process.
Instead of relying on developers to remember every rule, the system continuously checks them.
Common Mistakes
Mistake 1: Relying Only on Documentation
Documentation explains architecture but does not enforce it.
Mistake 2: Creating Too Many Rules
An architecture suite with hundreds of fragile rules becomes difficult to maintain.
Mistake 3: Blocking Legitimate Designs
Rules should represent actual architectural decisions, not personal preferences.
Mistake 4: Ignoring Existing Violations
If the current codebase already violates a rule, introducing the gate may immediately block every pull request.
Mistake 5: Writing Tests That Are Too Fragile
Architecture tests should survive reasonable refactoring.
Mistake 6: Treating Architecture Tests as Business Tests
They solve different problems.
Mistake 7: Assuming AI Will Follow Instructions Forever
Automated enforcement is more reliable than instructions alone.
Introducing Gates Into a Legacy Application
Large applications often already contain architectural violations.
Do not try to fix everything at once.
Instead, establish a baseline.
For example:
Current Violations
|
v
Document Baseline
|
v
Block New Violations
|
v
Reduce Existing Violations
This is sometimes called a ratcheting approach.
The key idea is:
Do not allow the architecture to become worse while gradually improving it.
Handling Existing Violations
Suppose the current codebase has:
15 Domain -> Infrastructure violations
A new architecture gate can initially identify those violations.
Then the team can establish a rule such as:
New violations = 0
The existing technical debt can be addressed separately.
This is often more practical than requiring a complete architectural rewrite before automation can be introduced.
Testing AI-Generated Pull Requests
A useful experiment is to compare ordinary and AI-generated pull requests.
For each pull request, record:
Build Result
Test Result
Architecture Result
Security Result
Review Comments
Rework Required
Then categorize architecture failures:
Layer Violation
Namespace Violation
Naming Violation
Dependency Violation
Pattern Violation
This helps the engineering team understand where AI-generated changes need additional guardrails.
Architecture Quality Score
Organizations can create an internal scorecard.
For example:
| Check | Result |
|---|
| Build | Pass |
| Unit Tests | Pass |
| Integration Tests | Pass |
| Architecture | Pass |
| Security | Pass |
| Code Quality | Pass |
| Human Review | Pending |
The pull request becomes:
Ready for Merge
only after the required checks pass.
The exact rules should be determined by the organization.
Advantages
Prevents Architectural Drift
Rules are continuously checked instead of relying only on developer discipline.
Useful for AI-Generated Code
AI agents can generate many files quickly, making automated structural validation valuable.
Faster Code Review
Reviewers can spend less time checking basic dependency rules.
Repeatable
The same rules run on every pull request.
Works With Existing .NET Workflows
Architecture tests can run alongside normal build and test processes.
Makes Architecture Executable
Architectural decisions become machine-verifiable rules.
Disadvantages and Limitations
Initial Setup Takes Time
The team must define meaningful architectural rules.
Existing Technical Debt Can Cause Failures
Legacy projects may contain many violations.
Rules Require Maintenance
Architecture evolves, so tests must evolve too.
Not Everything Can Be Automated
Business architecture and design intent sometimes require human judgment.
Poor Rules Create Friction
Overly restrictive tests can block legitimate development.
Architecture Tests Do Not Guarantee Good Software
A project can follow dependency rules and still contain incorrect business logic.
Recommended Architecture Gate Strategy
For a typical .NET application, start with a small set of high-value rules:
1. Domain cannot depend on Infrastructure.
2. Domain cannot depend on API.
3. Application cannot depend on API.
4. Controllers cannot directly access DbContext.
5. Infrastructure dependencies remain behind defined abstractions.
6. Required naming conventions are enforced.
7. New projects must follow approved dependency direction.
Once these rules are stable, add more specialized checks.
A Complete AI-Assisted Pull Request Workflow
A mature workflow can look like this:
Developer
|
v
AI Agent
|
v
Code Changes
|
v
Pull Request
|
+-------------------+
| |
v v
Build Unit Tests
| |
+---------+---------+
|
v
Integration Tests
|
v
Architecture Gates
|
v
Security Checks
|
v
Code Quality
|
v
Human Review
|
v
Merge
The important idea is that AI-generated code enters the same engineering control system as human-generated code.
The source of the code does not change the quality requirements.
Practical Implementation Checklist
Before introducing architecture gates, define:
[ ] What is the intended architecture?
[ ] Which project dependencies are allowed?
[ ] Which dependencies are prohibited?
[ ] Which namespaces have restrictions?
[ ] Which naming conventions matter?
[ ] Which framework dependencies are restricted?
[ ] Which rules should block a pull request?
[ ] Which existing violations are accepted temporarily?
[ ] How will architecture tests run in CI?
[ ] How will violations be reported?
[ ] Who owns the architecture rules?
[ ] How will the rules change when architecture evolves?
Once these questions are answered, implementation becomes much easier.
Conclusion
AI-generated code does not remove the need for architecture; if anything, it makes automated architectural validation more valuable. An AI agent can generate a working implementation very quickly, but a working implementation can still violate dependency boundaries, introduce unwanted framework coupling, bypass application layers, or create long-term architecture drift. Architecture gates turn important design rules into executable tests that can run automatically during every pull request. For .NET applications, this can include checking project dependencies, namespace boundaries, layer relationships, controller responsibilities, framework dependencies, naming conventions, and other structural rules. The best approach is to start with a small set of meaningful rules, run them alongside builds and tests, and gradually expand them as the team gains confidence. The goal is not to create more CI failures. The goal is to make the architecture easier to protect, especially when developers and AI agents are both making changes at a much faster pace.