Architecture fitness functions: tests that fail the build when your layers rot
Every architecture decision document ends the same way: a diagram, a few rules in prose, and a shared understanding that we will all respect the boundaries. Domain does not depend on infrastructure. Handlers live in the application layer. Everything is sealed.
Then someone, six months and forty pull requests later, needs a quick fix on a Friday and adds
using Acme.Infrastructure; to a domain class. The build is green. The tests pass. The PR gets a
thumbs up because the reviewer is looking at the bug fix, not auditing import statements. The rule
is now broken, and nothing on earth told anyone.
This is the central, depressing fact about software architecture: architecture you do not enforce automatically will erode. Not might. Will. Discipline is a renewable resource that runs out under deadline pressure, and prose in a wiki has never once failed a build.
The fix is to make the rule executable
A fitness function is an automated test for a structural property of your system, the same idea as
a unit test, aimed at architecture instead of behavior. Instead of asserting that Add(2, 2)
returns 4, it asserts that the domain assembly does not reference the infrastructure assembly. It
is a test. It runs in CI. It is red or green. There is no judgment call and no Friday exception.
The architecture suite
Layers only depend inward
Infrastructure -> Domain only
✓ holding
Handlers are named and sealed
every IRequestHandler implementation
✓ holding
Domain must not depend on Infrastructure
Acme.Domain.Order -> Acme.Infrastructure
✗ 1 violation
In .NET, the cleanest way to write these is ArchUnitNET, a port of the Java ArchUnit library. You load your assemblies once, then express rules as fluent assertions inside ordinary xUnit tests:
using ArchUnitNET.Loader;
using ArchUnitNET.Fluent;
using ArchUnitNET.xUnit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
public sealed class ArchitectureTests
{
private static readonly Architecture Architecture =
new ArchLoader()
.LoadAssemblies(
typeof(Domain.Order).Assembly,
typeof(Infrastructure.OrderRepository).Assembly,
typeof(Application.PlaceOrderHandler).Assembly)
.Build();
[Fact]
public void Domain_should_not_depend_on_infrastructure()
{
IArchRule rule = Types()
.That().ResideInNamespace("Acme.Domain", useRegularExpressions: false)
.Should().NotDependOnAny(
Types().That().ResideInNamespace("Acme.Infrastructure"));
rule.Check(Architecture);
}
}
That single test is worth more than a paragraph in a design doc, because it cannot be ignored. The
moment someone adds the forbidden using, this test goes red and the merge is blocked. The rule
defends itself.
Layering, naming, and sealing in the same place
Dependency direction is the headline rule, but the same mechanism handles the rest of the conventions teams usually only hope for. You can assert the layer cake holds:
[Fact]
public void Layers_should_only_depend_inward()
{
IArchRule rule = Types()
.That().ResideInNamespace("Acme.Infrastructure")
.Should().DependOnAny(Types().That().ResideInNamespace("Acme.Domain"))
.AndShould().NotDependOnAny(
Types().That().ResideInNamespace("Acme.Application"));
rule.Check(Architecture);
}
You can enforce the naming and sealing conventions that code review otherwise polices by hand and inconsistently:
[Fact]
public void Handlers_should_be_named_and_sealed()
{
IArchRule rule = Classes()
.That().ImplementInterface(typeof(IRequestHandler<,>))
.Should().HaveNameEndingWith("Handler")
.AndShould().BeSealed();
rule.Check(Architecture);
}
None of these are exotic. They are the rules already written in your team's onboarding doc. The only change is that they have teeth now.
A practical note on adoption: do not try to assert every rule on day one of a legacy codebase, or you will drown in a thousand red assertions and turn the whole thing off in disgust. Pick the one rule that hurts most, usually domain-to-infrastructure direction, get it green by fixing the real violations, and freeze it there. Add the next rule once the first one has been holding for a sprint. Fitness functions are a ratchet, not a big-bang migration. Each one you lock in is a class of decay that can never come back.
The before and after, felt
Consider the difference in lived experience.
Before: dependency rules live in a wiki page. New hire reads it on day three, forgets it by week two. Over a year, eleven domain classes quietly grow infrastructure references. Nobody notices until a "simple" extraction of the domain into its own package turns into a three-week untangling, because the domain no longer compiles without EF Core, Serilog, and a Redis client dragged along.
After: the same rule is one ArchUnitNET test in CI. The first developer who adds the wrong
usingsees a red build in ninety seconds, fixes it before the PR is even reviewed, and the domain stays clean for the entire year. The three-week untangling never happens because the tangle was never allowed to form.
The cost is a handful of tests written once. The return is that your architecture stops being a story you tell and becomes a property the build guarantees. That is a genuinely different category of confidence.
Practicing the thing CI will judge
There is a catch worth naming. Fitness functions tell you when you broke a rule, but they do not
teach you to write code that satisfies the rule in the first place. If your instinct is to reach for
the DbContext from a handler, you will spend your first months fighting red builds instead of
shipping. The skill that makes fitness functions feel like a safety net rather than a nag is having
internalized the rules before you meet them in anger.
That is exactly what Katabench's architecture track trains. The multi-file katas are graded with these same fitness functions, ArchUnitNET over your submitted assembly, so you are practicing writing code that satisfies "domain must not depend on infrastructure" and the layering and naming rules right alongside the behavioral tests. The assembly is parsed statically and never executed for the architecture checks, so the structural grade is clean and deterministic. You can read the full breakdown of how grading works if you want the details.
The point is the loop. Write the code, get the structural verdict immediately, adjust, repeat, until pointing your dependencies the right way is not a rule you remember but a habit you have. Architecture you enforce automatically holds. The question is only whether you build that reflex on a kata or in production, after the tangle has already formed. The architecture track is the cheaper place to learn it.