open atlas
↑ Back to track
Architecture Patterns ARCH · 11 · 02

Fitness Functions and Evolutionary Architecture

A fitness function is an automated test that an architectural property still holds. Fitness functions let architecture evolve incrementally under protection — the wrong-way detector that fails the build before a violation reaches production.

ARCH Senior ◷ 22 min
Level
FoundationsJuniorMiddleSenior

Eighteen months after the B2B billing platform established its modular monolith with enforced boundaries, the team hired twelve new engineers. The module rules were in an ADR and in an onboarding doc. The ESLint import restriction rules existed — but nobody had checked them in four months because the lint step had been accidentally removed from the CI pipeline during a Dockerfile refactor. Nobody noticed, because the build still passed.

By the time someone ran the architecture tests manually, there were 23 boundary violations across six modules. Billing was importing from ordering internals. The notifications module was calling the inventory module’s repository layer directly. Worse: the violations had been deployed to production. The architectural structure the team had spent months building had been eroded — silently, incrementally, one shortcut at a time — while the CI pipeline reported green.

The problem was not the rules. The problem was that nothing was continuously measuring whether the rules still held.

What a fitness function is

The term comes from Neal Ford, Rebecca Parsons, and Patrick Kua’s book Building Evolutionary Architectures (O’Reilly, 2017). They borrowed it from evolutionary computing, where a fitness function scores how well a candidate solution satisfies the problem’s requirements.

In architecture, a fitness function is any mechanism that provides an objective, automated assessment of whether an architectural characteristic still holds. It is not documentation. It is not a code review checklist. It runs in CI and produces a pass or fail.

Architectural characteristics — sometimes called “ilities” — are the non-functional properties the architecture must preserve:

  • Modularity: no module imports from another module’s internals
  • Latency: the p99 response time of the billing API is under 200ms
  • Cyclic dependency freedom: no package-level cycles between modules
  • Security: no sensitive data appears in log output
  • Scalability: the system can process 10,000 invoice generations per minute

A fitness function turns these from aspirations into build gates.

Atomic vs holistic fitness functions

Ford, Parsons, and Kua distinguish two dimensions of fitness functions: scope and frequency.

Atomic fitness functions test a single architectural characteristic in isolation:

  • “No cyclic dependencies between modules” — a single ArchUnit rule
  • “No direct imports from another module’s internal package” — a ts-arch or ESLint rule
  • “All public API methods return within 50ms in unit tests” — a performance assertion in the test suite

Holistic fitness functions test a combination of characteristics that only emerge together:

  • “Under a 10,000 RPM load, the system maintains p99 < 200ms AND error rate < 0.1% AND no OOM errors” — a load test in CI
  • “The deployed system passes security scanning AND dependency audit AND no cross-boundary access in production traces” — a composite gate

Most teams start with atomic fitness functions because they are cheap to write and fast to run. Holistic functions are more expensive but catch emergent failure modes that atomic tests miss.

Why this works

Why is this called “evolutionary architecture”? Because fitness functions enable guided incremental change. Without fitness functions, the architecture degrades through accumulated shortcuts — each individual change is small and defensible, but the cumulative effect is erosion. With fitness functions, each change is evaluated against the architectural properties the team committed to. The architecture can evolve — new patterns added, old ones removed — but only in directions that preserve the properties that matter. The fitness function is what makes change safe. It is the difference between “we evolved the architecture on purpose” and “the architecture drifted until we noticed.”

Quiz

The billing platform's architecture team wants to add a fitness function to prevent the boundary violations that silently crept in. They have two proposals: (A) Add an ArchUnit test that asserts 'no class in billing.internal may import from ordering.internal' — runs in the unit test suite on every commit. (B) Add an architectural review checklist that senior engineers fill out in every PR. Which is a fitness function, and why does it matter?

Triggered vs continuous fitness functions

The second dimension in Ford/Parsons/Kua is frequency:

Triggered fitness functions run on demand — in CI, on merge, on a schedule. They are appropriate for checks that are expensive to run (load tests, full security scans, integration tests against a staging environment) or that test properties that can only be verified against a built artifact.

Continuous fitness functions run on every change, in the development loop, as fast as possible. They are appropriate for structural checks (dependency rules, cyclic dependencies, import restrictions) that can be evaluated against source code without building or deploying.

For the billing platform’s boundary violation problem, a continuous fitness function is correct: the ArchUnit or ts-arch check runs in the unit test suite on every commit, failing immediately when a violation is introduced. There is no lag between violation and detection.

A load test that verifies p99 < 200ms under 10,000 RPM is necessarily triggered — it requires a running environment and takes minutes to execute. It runs nightly or before a release, not on every commit.

Quiz

The billing team has three proposed fitness functions: (1) An ArchUnit rule asserting no cyclic module dependencies — takes 2 seconds in the test suite. (2) A load test asserting p99 < 150ms under 5,000 concurrent invoice requests — takes 8 minutes against a staging environment. (3) A dependency vulnerability scan against the full dependency tree — takes 30 seconds. How should these be classified as triggered vs continuous, and at what frequency should each run?

Implementing fitness functions: practical examples

ArchUnit (Java) — dependency direction:

// Fail the test if any class in billing imports from ordering.internal
@AnalyzeClasses(packages = "com.platform")
class ModuleBoundaryTest {
  @ArchTest
  static final ArchRule billingMayNotImportOrderingInternals =
    noClasses().that().resideInAPackage("..billing..")
      .should().dependOnClassesThat()
      .resideInAPackage("..ordering.internal..");
}

ts-arch (TypeScript) — no cycles:

// Fail the test if any circular dependency exists between modules
describe("architecture", () => {
  it("has no cyclic module dependencies", async () => {
    const result = await tsarch.FileSystem
      .fromConfig("tsconfig.json")
      .onlyFiles(["src/**/*.ts"])
      .noCycles();
    expect(result).toEqual([]);
  });
});

Latency budget (unit test assertion):

it("invoice generation completes within 100ms", async () => {
  const start = performance.now();
  await billingService.generateInvoice(testOrderId);
  const elapsed = performance.now() - start;
  expect(elapsed).toBeLessThan(100);
});

The commonality: each is a test that produces a pass/fail. Each runs in CI. Each catches a specific architectural property. The property was previously only maintained by human convention; the fitness function makes it a machine-enforced invariant.

lesson.inset.note

The boundary violations in the billing platform were caught eventually — but only after they had been in production for months. The cost of fixing them was proportional to how long they had existed: six modules had violations, some of them load-bearing. Had a fitness function been running continuously, each violation would have been caught within hours of being introduced, when only one engineer’s change was involved. The cost of fixing a violation is O(1) when caught immediately; it grows roughly linearly with the number of changes stacked on top of it.

Quiz

After fixing the 23 boundary violations, the billing platform team considers adding fitness functions. A junior engineer asks: 'Can we just add the fitness functions now without refactoring the violations? Then the fitness functions will fail and we can fix them as we go.' Why is this reasoning flawed?

Recall before you leave
  1. 01
    What is a fitness function, and what distinguishes it from a code review checklist or an architectural document?
  2. 02
    What is the difference between atomic and holistic fitness functions? Give an example of each.
  3. 03
    Why does 'evolutionary architecture' require fitness functions rather than just good documentation and senior review?
Recap

The billing platform’s silent erosion — 23 boundary violations accumulated over months while CI reported green — is the failure mode fitness functions prevent. The violations were not introduced by careless engineers; they were introduced by engineers who had no immediate feedback that a boundary was being crossed.

A fitness function turns an architectural intention into a machine-enforced invariant. The ArchUnit test does not trust engineers to remember the rules; it catches violations the moment they are introduced, before any code review, before any deployment, while the change is still local and cheap to fix.

Evolutionary architecture is not architecture that drifts. It is architecture that changes on purpose, guided by fitness functions that define the properties worth preserving. The fitness function is the complement to the ADR: the ADR records why a property was chosen; the fitness function enforces that the property is still being maintained.

The practical sequence: write the ADR to record the decision, implement the fitness function to enforce it, run the fitness function continuously to catch drift. When the fitness function starts failing for a principled reason — the forces have changed, a new pattern supersedes the old — write a new ADR to supersede the original, update the fitness function, and proceed.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 3 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.