Reference·Quality Attributes

Architecture Fitness Functions Catalog

Sixty automated checks that turn architectural intentions into a build that fails — coupling, latency, resilience, security, cost, and LLM evals.

Last reviewed ·revision 1·first published

This is a living document. It is revised when the underlying numbers or practice change, not on a publishing schedule.

Show revision history
  • First publication. Eleven attribute sections with metric, threshold and tooling per function, plus an adoption sequence and a five-item starter set.

14 min
Architecture Fitness Functions Catalog
In short

An architecture fitness function is an automated test whose subject is a quality attribute rather than a feature. Each one needs five parts — characteristic, metric, threshold, trigger and response. Introduce them by ratcheting from the current measured value rather than gating on an ideal, give every function an owner, and delete any that has not fired in a year.

An architecture fitness function is an automated test whose subject is an architectural characteristic rather than a feature. "Payments must reconcile" is a test. "No module in the domain layer may import from the web layer" is a fitness function.

The distinction matters because architectural characteristics are the ones that degrade invisibly. Nobody ships a commit that says "make this less modular." They ship forty commits, each locally reasonable, and eighteen months later the layering is gone and the only evidence is that everything takes longer.

A fitness function converts an architectural intention from a document nobody re-reads into a build that fails.

Anatomy

Every usable fitness function has five parts. Missing any one of them is why most attempts die.

PartQuestionExample
CharacteristicWhich quality attribute?Modularity
MetricWhat number represents it?Count of imports crossing a forbidden boundary
ThresholdWhat value is acceptable?0 new; existing 14 grandfathered
TriggerWhen does it run?Every pull request
ResponseWhat happens on breach?Build fails; owning team named in the message

If you cannot fill in metric, the characteristic is not yet architectural — it is an aspiration. "The system should be maintainable" has no metric. "No file exceeds 600 lines and no function exceeds cyclomatic complexity 15" does, and it is a defensible proxy.

Classification

Two axes are worth knowing, because they tell you where a function can run.

  • Atomic vs. holistic. Atomic tests one characteristic in isolation (a coupling rule). Holistic tests characteristics in combination — the interesting failures live here, because caching improves latency and damages consistency at the same time.
  • Triggered vs. continuous. Triggered runs on a pipeline event. Continuous runs against production forever — a synthetic probe, a chaos schedule, an SLO burn-rate alert. Continuous functions catch what a pipeline cannot: reality.

A third axis, static vs. dynamic, decides cost. Static analysis is free and runs on every commit. Dynamic requires a deployed system, so it runs nightly or on merge. Push everything you can into static.


The catalog

Modularity and coupling

Fitness functionMetric & thresholdTooling
No layer violationsImports crossing a forbidden boundary = 0ArchUnit (JVM), NetArchTest (.NET), import-linter (Python), dependency-cruiser / eslint-plugin-boundaries (TS)
No cyclic dependencies between modulesCycle count = 0madge, dependency-cruiser, ArchUnit
Public API surface is deliberateExported symbols per module ≤ agreed listAPI Extractor, public-api.md snapshot diff
Module fan-in/fan-out ceilingEfferent coupling ≤ 12 per moduledependency-cruiser metrics, jdepend
Ownership is unambiguousFiles with no CODEOWNERS entry = 0CI script over the codebase

Highest leverage first function in almost any codebase: the layer rule. It is static, it is fast, it takes an afternoon, and it stops the single most common form of architectural erosion.

Performance

Fitness functionMetric & thresholdTooling
API latency budgetp95 ≤ 300 ms, p99 ≤ 800 ms on the smoke suitek6 / Gatling thresholds in CI
Frontend bundle budgetInitial JS ≤ 180 KB gzipped; regression > 5 KB failssize-limit, bundlesize, Next.js build output check
Core Web VitalsLCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1Lighthouse CI on representative routes
No N+1 queries introducedQuery count per canonical request ≤ baselineQuery-count assertions in integration tests
Startup timeCold start ≤ 400 msTimed boot in CI

Bundle budgets are the most under-used entry here. They are trivially automatable, the number is unarguable, and page weight is the characteristic most likely to regress from a dependency someone added without looking.

Resilience and availability

Fitness functionMetric & thresholdTooling
Every outbound call has a timeoutCalls with default/infinite timeout = 0Static lint rule or ArchUnit condition
Retry budget respectedRetries ÷ successful requests ≤ 10%Production metric with an alert
Dependency failure is survivableService stays within SLO with dependency X unavailableScheduled chaos experiment (Litmus, Gremlin, AWS FIS)
Graceful degradation is realNon-critical dependency down ⇒ no 5xxNightly integration run with the dependency stubbed as failing
No unbounded queueQueues without a maximum size = 0Config lint over service definitions

These pair directly with the distributed systems failure catalog — nearly every entry there has a fitness function that would have caught it before an incident did.

Security

Fitness functionMetric & thresholdTooling
No known-vulnerable dependenciesCritical/high CVEs = 0; medium ≤ baselineDependabot, Snyk, npm audit, Trivy
No secrets committedDetections = 0gitleaks, trufflehog, GitHub secret scanning
Infrastructure policy holdsPolicy violations = 0 (no public buckets, encryption on, no 0.0.0.0/0)OPA / Conftest, Checkov, tfsec
Licence policy holdsDisallowed licences = 0licence-checker, FOSSA
Security headers presentCSP, HSTS, X-Content-Type-Options on every responseAssertion in the smoke suite
Auth is not bypassableEndpoints without an auth decorator/middleware = 0Route-table introspection test

API and contract compatibility

Fitness functionMetric & thresholdTooling
No breaking API change without a version bumpBreaking diffs = 0oasdiff (OpenAPI), buf breaking (protobuf)
Consumers still passContract verification green for every registered consumerPact broker, can-i-deploy
Events match their registered schemaSchema compatibility = BACKWARDConfluent Schema Registry compatibility check
Deprecated endpoints actually retireEndpoints past their sunset date = 0CI script over a deprecation manifest

Data

Fitness functionMetric & thresholdTooling
Migrations are backward compatibleDestructive DDL in a single deploy = 0 (expand/contract enforced)squawk, migration linter
Every table has an owner and a retention policyUntagged tables = 0Catalogue check
PII is labelled and encryptedUnlabelled columns matching PII heuristics = 0Schema scanner
No long-running lock in a migrationEstimated lock time ≤ 1 ssquawk, explain-based check

Observability

Fitness functionMetric & thresholdTooling
Every endpoint is tracedHandlers without a span = 0OpenTelemetry instrumentation audit
Every log line carries a correlation idSampled lines missing trace id = 0Log-pipeline assertion
Every service has an SLO and a burn alertServices without both = 0Config check over the SLO repository
Dashboards are not orphanedDashboards referencing dead metrics = 0Scheduled metric-existence check

Cost

Fitness functionMetric & thresholdTooling
Infrastructure cost delta per PRMonthly delta ≥ $200 requires approvalInfracost in the PR comment
Cost per thousand requestsWithin 10% of baselineBilling export ÷ request count, weekly
LLM spend per request≤ $0.004 median; alert at 1.5xToken accounting in the gateway
No idle expensive resourcesResources under 5% utilisation for 14 days = 0Scheduled cloud audit

Maintainability

Fitness functionMetric & thresholdTooling
Changed-line coverage≥ 80% on the diff, not the repodiff-cover, Codecov patch status
Complexity ceilingCyclomatic complexity ≤ 15 per functioneslint-complexity, radon, PMD
File size ceiling≤ 600 linesLint rule
Duplication≤ 3% and non-increasingjscpd, PMD CPD
Build stays fastCI wall time ≤ 12 min p95Pipeline metric with an alert
No flaky tests accumulateTests failing intermittently over 7 days = 0Test-result history analysis

Repository-wide coverage is the classic bad metric — it moves too slowly to influence a decision and it punishes the person who touched the file. Changed-line coverage is the version that changes behaviour.

Accessibility

Fitness functionMetric & thresholdTooling
No WCAG violations on key routesaxe critical/serious = 0axe-core, Playwright + axe
Contrast ratio≥ 4.5:1 for body textAutomated token audit
Keyboard reachabilityInteractive elements unreachable by keyboard = 0Playwright traversal

AI and agent systems

The newest section, and the one with the least established practice — which is precisely why the functions are worth writing down.

Fitness functionMetric & thresholdTooling
Eval suite pass rate≥ 92%, no regression on the golden setYour eval harness, run in CI
Grounding / citation rateAnswers with a valid source ≥ 95%Automated judge over the eval set
Tool-call schema validityMalformed tool calls = 0Schema validation in the harness
Prompt token budgetSystem prompt + context ≤ 60% of the windowStatic token count in CI
Latency budgetp95 time-to-first-token ≤ 900 msLoad test against the deployed endpoint
Cost per successful task≤ target; regression fails the buildEval harness with token accounting
Injection resistanceKnown prompt-injection suite blocked = 100%Adversarial eval set

If you are building on LLMs and your CI does not fail on an eval regression, you do not have a fitness function — you have a demo with a deploy button.


Introducing them without a revolt

Ratchet, do not gate. Measure the current value. Set the threshold at it. Forbid regression. Tighten quarterly. A rule that fails the build on day one for 400 pre-existing violations gets disabled on day two, and the second attempt is much harder than the first.

One owner per function. A red build with no owner becomes a red build everyone ignores, and then a red build everyone bypasses.

Fail with the reason, not the number. Coupling violation: domain/order imports web/session. The domain layer must not depend on transport. See ADR-014. A message that just prints a metric produces a workaround; a message that explains the intent produces a design conversation.

Budget five minutes. Anything slower gets moved to nightly, and nightly failures get triaged on Friday, which means never.

Delete the ones that never fire. A fitness function that has not failed in a year is either protecting something nobody threatens or measuring something that cannot regress. Both are maintenance cost with no return.

The starter set

If you are adding fitness functions to an existing system this week, these five give the most protection for the least effort, in this order:

  1. Layer/dependency rule — stops the fastest form of erosion, static, minutes to run.
  2. Changed-line coverage — makes new code testable without relitigating the past.
  3. Dependency CVE + secret scanning — the cheapest security posture available.
  4. Bundle size or latency budget — whichever your users feel.
  5. Every outbound call has a timeout — one grep-shaped rule that pre-empts an entire class of outage.

Everything else in this catalog is worth having. None of it is worth having before these.

Sources

Frequently asked

What is an architecture fitness function?

An automated test whose subject is an architectural characteristic rather than a feature — for example, that no module in the domain layer may import from the web layer, or that p95 latency stays under 300 ms. The term comes from evolutionary architecture, and the point is to make characteristics that normally degrade invisibly fail a build instead.

How is a fitness function different from a normal test?

A normal test asserts behaviour: given this input, expect this output. A fitness function asserts a property of the system as a whole: coupling, latency, bundle size, dependency freshness, cost per request. Fitness functions are usually cross-cutting, often static rather than dynamic, and frequently run continuously against production rather than only in the pipeline.

How do you add fitness functions to an existing codebase without failing every build?

Ratchet rather than gate. Measure the current value, set the threshold at it, forbid regression, and tighten the threshold on a schedule. A rule that fails on day one for four hundred pre-existing violations gets disabled on day two, and the second attempt is much harder than the first.

Which fitness functions should a team add first?

A layer or dependency rule, changed-line test coverage, dependency CVE plus secret scanning, one performance budget (bundle size or API latency), and a rule that every outbound call has a timeout. These five are static or near-static, run in minutes, and each pre-empts a whole class of failure.

What are fitness functions for LLM and agent systems?

Eval suite pass rate with no regression on a golden set, grounding or citation rate, tool-call schema validity, a prompt token budget as a share of the context window, a time-to-first-token latency budget, cost per successful task, and a prompt-injection resistance suite. If CI does not fail on an eval regression, the system has no fitness function protecting its behaviour.

Why is repository-wide test coverage a poor fitness function?

It moves too slowly to influence any individual decision and it penalises whoever happens to touch a poorly covered file. Coverage measured on the changed lines of a diff moves immediately, is attributable to the change that caused it, and does not require relitigating the past.

Go deeper