DEV Community

Cover image for Your Clean Architecture Diagram Is Decoration Until CI Breaks It
MICHAEL-MAURICE
MICHAEL-MAURICE

Posted on

Your Clean Architecture Diagram Is Decoration Until CI Breaks It

Every Clean Architecture repo I've joined had a pretty onion diagram in the README. Six months later someone needed an exchange rate inside an entity, typed new HttpClient(), the build stayed green, and the diagram became wallpaper.

Code review is supposed to catch that. Reviewers read the logic in the diff, not which namespaces quietly appeared at the top of a Domain file.

Turn the diagram into tests. This is the short "what actually fails in production teams" checklist with NetArchTest.Rules on .NET 10 — not a full scaffold walkthrough. The complete layered sample (seven rules, deliberate break, green suite) ships with the Clean Architecture .NET 10 Web API Starter. New tested projects also land daily in Tech Skill Builder — $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10.

What project references already protect (and what they don't)

Project references stop cycles. Try pointing Application back at Infrastructure and the restore graph fails. Good.

They do not stop:

  • A NuGet package landing in Domain (System.Net.Http, EF Core, Redis client)
  • API types reaching into Infrastructure internals when the team agreed "only through Application"
  • Naming / sealing conventions drifting file by file

That gap is what architecture tests fill. They inspect compiled assemblies, name the offending type, and run on every CI build.

The four layers you're protecting

Domain          → nothing outward
Application     → Domain only
Infrastructure  → Application (+ Domain via Application)
Api             → Application (+ Infrastructure for DI wiring)
Enter fullscreen mode Exit fullscreen mode

Anchor each assembly with a real type. Loading by string name passes every rule when the rename empties the type list:

private static readonly Assembly DomainAssembly =
    typeof(CleanArch.Domain.Orders.Order).Assembly;
// full implementation in the complete project
Enter fullscreen mode Exit fullscreen mode

Checklist: five rules that catch real mistakes

1. Domain depends on nobody in your solution

[Fact]
public void Domain_Should_Not_Depend_On_Any_Other_Layer() =>
    AssertSuccess(Types.InAssembly(DomainAssembly)
        .ShouldNot().HaveDependencyOnAny(Application, Infrastructure, Api)
        .GetResult());
Enter fullscreen mode Exit fullscreen mode

2. Domain does no I/O or persistence

This is the HttpClient / DbContext rule from the intro:

[Fact]
public void Domain_Should_Not_Do_IO_Or_Persistence() =>
    AssertSuccess(Types.InAssembly(DomainAssembly)
        .ShouldNot().HaveDependencyOnAny(
            "System.Net.Http",
            "Microsoft.EntityFrameworkCore")
        .GetResult());
Enter fullscreen mode Exit fullscreen mode

Extend the deny-list with whatever your Domain must never see: ASP.NET Core, StackExchange.Redis, your bus client.

3. Application never sees Infrastructure or Api

[Fact]
public void Application_Should_Not_Depend_On_Infrastructure_Or_Api() =>
    AssertSuccess(Types.InAssembly(ApplicationAssembly)
        .ShouldNot().HaveDependencyOnAny(Infrastructure, Api)
        .GetResult());
Enter fullscreen mode Exit fullscreen mode

4. Handlers stay sealed in Application

[Fact]
public void Handlers_Should_Be_Sealed_And_Live_In_Application() =>
    AssertSuccess(Types.InAssembly(ApplicationAssembly)
        .That().HaveNameEndingWith("Handler")
        .Should().BeSealed()
        .And().ResideInNamespaceStartingWith(Application)
        .GetResult());
Enter fullscreen mode Exit fullscreen mode

5. Repository ports start with I

[Fact]
public void Repository_Interfaces_Should_Start_With_I() =>
    AssertSuccess(Types.InAssembly(ApplicationAssembly)
        .That().AreInterfaces()
        .Should().HaveNameStartingWith("I")
        .GetResult());
Enter fullscreen mode Exit fullscreen mode

Add Infrastructure→Api and "Domain entities sealed" if your team cares. Seven rules is a sweet spot; fifty rules nobody reads.

Break one on purpose

Drop this into Domain and re-run:

// FORBIDDEN — architecture test should fail
public static class Rates
{
    public static async Task<decimal> GetAsync(string currency)
    {
        using var http = new HttpClient();
        // full implementation in the complete project
        return 1m;
    }
}
Enter fullscreen mode Exit fullscreen mode

The failure message should list CleanArch.Domain...Rates. If CI stays green, your assembly anchor is wrong or the rule never ran.

Failure helper worth copying

private static void AssertSuccess(TestResult result) =>
    Assert.True(result.IsSuccessful,
        "Violating types: " + string.Join(", ", result.FailingTypeNames ?? []));
Enter fullscreen mode Exit fullscreen mode

Empty failing-name lists are useless in a PR comment. Name the type.

What to put in CI

Run the architecture test project on every PR. No special tooling — it's a normal dotnet test. Keep the rules next to the solution so a new teammate can open LayerTests.cs and learn the house rules in two minutes.

Want the full starter?

The complete four-layer sample plus these rules (and the deliberate break) is part of the Clean Architecture .NET 10 Web API Starter — CQRS, MediatR, JWT, Redis, RabbitMQ, ready for you to extend.

For a new tested .NET + AI project every day (full source + PDF), Tech Skill Builder is $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10.

Which layer rule has saved your team the most headaches? Drop it in the comments.

Top comments (0)