A team can agree on a sensible architecture and still watch it erode one small change at a time. A developer imports an internal module because it is convenient. Another adds a direct dependency across layers. Months later, the diagram still shows clean boundaries, but the code no longer follows them.

Code review can catch these changes, but reviewers must remember every architectural rule and notice every violation. For properties that matter repeatedly, memory is a weak enforcement mechanism.

An architecture fitness function is an automated check that evaluates whether a system still has a desired architectural property. The idea is simple: if a rule matters enough to preserve, and software can evaluate it reliably, make the rule executable.

This article explains how to turn architectural intent into focused checks, how to choose useful properties, and how to avoid tests that freeze the design instead of protecting it.

Treat architecture as properties to preserve

Architecture is not only a diagram of components. It also includes constraints on how those components may interact.

Suppose an application has three areas:

presentation -> application -> domain

The arrows describe allowed dependency direction. Presentation code may depend on application code, and application code may depend on domain code. The domain should not depend on presentation details.

A document can state that rule. A fitness function can continuously ask whether the code still satisfies it:

for each dependency from module A to module B:
    assert dependency_is_allowed(A, B)

The pseudocode is deliberately generic. Different languages expose dependencies through imports, package metadata, compiled artifacts, or static-analysis tools. The important idea is not the tool. It is the conversion of a design decision into a repeatable pass-or-fail check.

When a new dependency violates the rule, the build reports the problem near the change that introduced it. That shortens the distance between architectural drift and corrective action.

Start with one costly failure mode

A useful fitness function protects a property whose violation has a meaningful engineering cost.

Imagine a billing system where domain code must remain independent of a particular web framework. The team wants this separation because domain rules are reused by background jobs and command-line tools.

The smallest useful check is not “verify good architecture.” That goal is too vague. A testable rule is narrower:

domain modules must not depend on web-framework modules

Now the consequence is explicit. If a developer imports a web request type into a domain service, the check fails. The developer can then move request-specific translation to the boundary and pass plain domain data inward.

This is the central mental model:

important architectural property
            |
            v
      observable evidence
            |
            v
       automated check

Begin with the property, not with a testing tool. Otherwise it is easy to collect measurements that are automated but do not protect anything the team actually cares about.

Make the rule precise enough to disagree with

Architectural statements often sound clear until someone tries to implement them.

Consider this rule:

Services should be loosely coupled.

A machine cannot evaluate “loosely” without a concrete definition. The team first needs to decide what evidence would demonstrate the property it wants.

Depending on the design, a precise rule might be:

checkout may call pricing only through the PricingPort interface

or:

packages under domain/ may import only domain/ and the standard library

or:

no dependency cycle may exist between top-level modules

These rules protect different things. The first protects a specific abstraction boundary. The second restricts dependency direction. The third protects acyclic module relationships.

Writing the executable rule therefore forces a useful design conversation: what exactly are we trying to keep true, and why? If the team cannot answer that question, automating the rule is premature.

Check outcomes rather than incidental structure

Fitness functions become harmful when they encode details that do not represent important architectural properties.

Suppose a team decides that payment logic should be isolated from user-interface code. A check that forbids payment modules from depending on UI modules directly expresses that boundary.

A different check might require exactly seven payment classes with prescribed names. That rule may pass today, but class count and naming are usually implementation details. A harmless refactoring could fail the test even though the architectural boundary remains intact.

Prefer checks with this shape:

protect: dependency direction, isolation, compatibility, or another deliberate property
avoid:   arbitrary file layout, class counts, or temporary implementation shape

The distinction matters because architecture should constrain decisions that have system-level consequences without preventing local designs from evolving.

Use different kinds of evidence for different properties

Not every architectural property appears in the same place.

A dependency rule can often be checked statically because imports or compiled dependencies reveal the relationship without running the system. A latency objective, by contrast, requires runtime measurements. A compatibility rule may require exercising an interface against representative consumers.

The general process stays the same:

  1. State the property.
  2. Identify evidence that would show whether it holds.
  3. Decide where that evidence can be measured reliably.
  4. Fail or warn when the result crosses the agreed boundary.

For example, a team might care that a component does not depend on another component. Static dependency analysis is appropriate because the property is structural.

If the team instead cares that a request path stays below an agreed latency threshold under a defined workload, dependency analysis cannot answer the question. The fitness function needs a controlled performance measurement, including a documented workload and environment. Without those conditions, a numerical threshold may produce noisy conclusions.

A fitness function is therefore a pattern for preserving a property, not a specific kind of test.

Decide whether failure should block a change

Not every useful architectural measurement should immediately reject a build.

A binary invariant is a good candidate for blocking. For example:

domain must not import presentation

There is little value in knowingly merging a new violation if the team has deliberately established that boundary.

Other properties are trends rather than hard invariants. Suppose the team measures dependency count between two modules while gradually separating them. A temporary increase may be understandable during migration, and a strict threshold could make legitimate work harder.

In that situation, reporting the measurement may be more useful than blocking every change. The team can tighten the rule when the migration reaches a stable point.

Choose enforcement based on the meaning of failure:

  • Block when a violation clearly breaks a property the system is expected to maintain now.
  • Warn or track when the measurement is diagnostic, noisy, or part of an intentional transition.
  • Remove the check when the property no longer represents an architectural decision.

The last point is important. Automated architecture rules are code and require maintenance just like other tests.

Keep feedback close to the change

A fitness function is most useful when developers see a failure while the relevant change is still easy to understand.

A structural rule that runs in seconds can usually run with normal automated tests. A costly system-level measurement may belong in a later pipeline stage. The right location depends on execution cost and the reliability of the signal.

When a check fails, its message should explain the violated property rather than merely expose tool internals. Compare:

Rule 14 failed: edge checkout.impl -> pricing.internal

with:

Checkout must depend on Pricing through its public boundary.
Found: checkout.impl -> pricing.internal

Both identify the same edge. The second also tells a developer what design constraint was broken and points toward the intended relationship.

Good feedback turns a fitness function from a mysterious gate into executable documentation.

Expect architecture to evolve

A passing fitness function does not prove that an architecture is good. It proves only that the properties encoded by the checks currently hold.

This limitation has two consequences.

First, important properties can remain unprotected. A system may pass every dependency rule while suffering from poor runtime isolation because no check measures that behavior.

Second, old rules can outlive their purpose. Imagine that two modules are intentionally merged after the team learns that maintaining their separation creates more complexity than value. A fitness function preserving the old boundary should change with that decision.

Treat each rule as the executable form of a current architectural decision:

decision changes
      |
      v
review property
      |
      +--> still important -> keep or revise check
      |
      +--> no longer valid -> remove check

The goal is not to make yesterday’s architecture permanent. The goal is to make unintentional architectural change visible.

Avoid common failure modes

The first common mistake is automating too many rules at once. A large rule set creates maintenance work before the team knows which constraints are valuable. Start with a small number of costly, recurring failure modes.

The second is encoding preferences as architecture. A naming convention may be useful, but it should not be presented as an architectural invariant unless violating it creates the architectural consequence the team is trying to prevent.

The third is using unstable measurements as hard gates. Performance and reliability measurements depend on workload and environment. If the measurement varies for reasons unrelated to the code change, developers learn to distrust or bypass the check. Stabilize the measurement or use it as a trend before making it blocking.

The fourth is adding exceptions without examining why they exist. A growing allowlist can hide erosion behind a passing build. Some exceptions are legitimate, especially during migrations, but they should have a clear reason and an intended lifetime.

Finally, do not assume automation replaces architectural discussion. Fitness functions preserve decisions; they do not make those decisions for the team.

When a fitness function is worth adding

A fitness function is especially useful when a property is important, repeatedly at risk, objectively observable, and expensive to verify manually on every change. Dependency direction, forbidden coupling, module cycles, and selected compatibility constraints often fit this shape.

A simpler approach is better when the rule is temporary, subjective, cheap to inspect, or still being explored. A short-lived refactoring may need a checklist rather than a permanent architecture test. An early design may need discussion before its boundaries are stable enough to encode.

Ask three questions before adding a check:

  1. What concrete failure are we trying to prevent?
  2. What evidence reliably reveals that failure?
  3. Will automating this check reduce future reasoning cost more than maintaining it adds?

If those answers are clear, the fitness function has a useful job.

Conclusion

Architecture drifts when important constraints exist only in diagrams, documents, and people’s memory. Architecture fitness functions reduce that risk by turning selected properties into executable feedback.

Start with one architectural failure that has caused real cost. State the property precisely, choose evidence that actually represents it, and automate the smallest reliable check. Make violations easy to understand, and revise the check when the architectural decision changes.

The practical goal is not an architecture that can never change. It is an architecture where important changes are deliberate rather than accidental.