practice

Architecture Fitness Test

also called Dependency Rule Test, Architecture Unit Test, Boundary Enforcement

An automated check in the build that fails when code violates the intended module dependency rules - the mechanism that decides whether architectural boundaries survive schedule pressure or erode within months.

modular-monolithboundariescienforcementcoupling

Architectural boundaries are usually expressed in a document and a diagram. Neither prevents an import statement. In a monolith, reaching across a boundary is a single line, the change is small enough to pass review, and the reviewer may not know the boundary was there.

An architecture fitness test encodes the intended structure as an executable rule — module A may not depend on module B; nothing outside this package may import its internals; the domain layer may not reference the web framework — and fails the build when the rule is broken.

Why it matters

This is the difference between boundaries that hold for years and boundaries that hold for months. Every mechanism that relies on people remembering degrades under schedule pressure, and schedule pressure is the normal condition.

The second reason is that it converts an architectural intention into feedback at the moment of the violation. A developer who learns in thirty seconds that their import is forbidden makes a different choice; a developer who learns in an architecture review three weeks later has already built on it, and the conversation is now about a sunk cost.

It also makes the intended architecture legible. The rules are a precise, current, executable statement of the structure — unlike a diagram, which is a statement of what someone intended at some point in the past.

Implementation patterns

  • Rules as code in the test suite or the build, running on every commit alongside ordinary tests.
  • Start by asserting the current structure, not the desired one, then tighten. A rule set that fails on day one is disabled on day one.
  • Layer rules — domain may not depend on infrastructure, nothing may depend on the web layer — which catch the most consequential violations.
  • Module dependency rules, expressing which modules may call which.
  • Encapsulation rules: only a module's designated public interface is importable from outside.
  • Database schema ownership enforced at the database level where possible, with permissions, since shared tables are the violation that is hardest to undo and a code-level rule does not catch a query.
  • An explicit, reviewed exception mechanism, so a legitimate violation is recorded with a reason rather than causing the rule to be deleted. A rule with no exception path is removed the first time it is genuinely wrong.
  • Rules owned and updated deliberately, since an architecture that evolves needs its rules to evolve with it.

Industry example

The technique is standard in the modular-monolith practice that emerged as a counterweight to reflexive microservice adoption, and tooling exists across ecosystems — ArchUnit in the Java world and equivalents in .NET, TypeScript, Python and Go — precisely because teams that chose a monolith needed a mechanism to keep it modular.

The argument is made most sharply by teams who deferred distribution deliberately: a well-bounded module can be extracted into a service in days, because the interface, the data ownership and the event flow already exist. That option only survives if the boundaries were enforced mechanically, which makes the fitness test the thing that preserves the ability to choose later.

Failure scenarios

  • No enforcement at all, so the documented architecture and the actual one diverge silently.
  • Rules written for the desired structure, failing immediately and being disabled.
  • No exception mechanism, so the first legitimate violation deletes the rule.
  • Code rules without database ownership enforcement, so modules stay separate while their tables merge.
  • Rules that become stale as the architecture evolves, blocking legitimate work and losing credibility.
  • Rules so numerous or intricate that nobody understands why a build failed.
  • Enforcement only in a nightly job rather than in the pull request, where the feedback would be actionable.

Trade-offs

Fitness tests add friction to legitimate changes. Restructuring modules means updating rules, and there is a real cost when the rules are wrong or when the architecture genuinely should change — which is why the exception path and deliberate rule ownership matter as much as the rules.

They also only check structure, not design quality. A codebase can satisfy every dependency rule and be poorly designed within each module; the tests prevent one specific kind of decay and say nothing about the rest.

The trade is a small ongoing maintenance cost and occasional friction in exchange for boundaries that actually exist. For a small codebase with a single team the rules add little. For a modular monolith intended to remain modular — or for any architecture whose value depends on a boundary being respected — it is the only mechanism that reliably works, because it is the only one that does not depend on anybody remembering.

Interview question

"We decided on a modular monolith with strict boundaries eighteen months ago and the modules now import each other freely. Tell me what should have been in place, what you would do now given the current state, and what you would enforce first."