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 historyHide revision history
First publication. Eleven attribute sections with metric, threshold and tooling per function, plus an adoption sequence and a five-item starter set.

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.
| Part | Question | Example |
|---|---|---|
| Characteristic | Which quality attribute? | Modularity |
| Metric | What number represents it? | Count of imports crossing a forbidden boundary |
| Threshold | What value is acceptable? | 0 new; existing 14 grandfathered |
| Trigger | When does it run? | Every pull request |
| Response | What 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 function | Metric & threshold | Tooling |
|---|---|---|
| No layer violations | Imports crossing a forbidden boundary = 0 | ArchUnit (JVM), NetArchTest (.NET), import-linter (Python), dependency-cruiser / eslint-plugin-boundaries (TS) |
| No cyclic dependencies between modules | Cycle count = 0 | madge, dependency-cruiser, ArchUnit |
| Public API surface is deliberate | Exported symbols per module ≤ agreed list | API Extractor, public-api.md snapshot diff |
| Module fan-in/fan-out ceiling | Efferent coupling ≤ 12 per module | dependency-cruiser metrics, jdepend |
| Ownership is unambiguous | Files with no CODEOWNERS entry = 0 | CI 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 function | Metric & threshold | Tooling |
|---|---|---|
| API latency budget | p95 ≤ 300 ms, p99 ≤ 800 ms on the smoke suite | k6 / Gatling thresholds in CI |
| Frontend bundle budget | Initial JS ≤ 180 KB gzipped; regression > 5 KB fails | size-limit, bundlesize, Next.js build output check |
| Core Web Vitals | LCP ≤ 2.5 s, INP ≤ 200 ms, CLS ≤ 0.1 | Lighthouse CI on representative routes |
| No N+1 queries introduced | Query count per canonical request ≤ baseline | Query-count assertions in integration tests |
| Startup time | Cold start ≤ 400 ms | Timed 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 function | Metric & threshold | Tooling |
|---|---|---|
| Every outbound call has a timeout | Calls with default/infinite timeout = 0 | Static lint rule or ArchUnit condition |
| Retry budget respected | Retries ÷ successful requests ≤ 10% | Production metric with an alert |
| Dependency failure is survivable | Service stays within SLO with dependency X unavailable | Scheduled chaos experiment (Litmus, Gremlin, AWS FIS) |
| Graceful degradation is real | Non-critical dependency down ⇒ no 5xx | Nightly integration run with the dependency stubbed as failing |
| No unbounded queue | Queues without a maximum size = 0 | Config 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 function | Metric & threshold | Tooling |
|---|---|---|
| No known-vulnerable dependencies | Critical/high CVEs = 0; medium ≤ baseline | Dependabot, Snyk, npm audit, Trivy |
| No secrets committed | Detections = 0 | gitleaks, trufflehog, GitHub secret scanning |
| Infrastructure policy holds | Policy violations = 0 (no public buckets, encryption on, no 0.0.0.0/0) | OPA / Conftest, Checkov, tfsec |
| Licence policy holds | Disallowed licences = 0 | licence-checker, FOSSA |
| Security headers present | CSP, HSTS, X-Content-Type-Options on every response | Assertion in the smoke suite |
| Auth is not bypassable | Endpoints without an auth decorator/middleware = 0 | Route-table introspection test |
API and contract compatibility
| Fitness function | Metric & threshold | Tooling |
|---|---|---|
| No breaking API change without a version bump | Breaking diffs = 0 | oasdiff (OpenAPI), buf breaking (protobuf) |
| Consumers still pass | Contract verification green for every registered consumer | Pact broker, can-i-deploy |
| Events match their registered schema | Schema compatibility = BACKWARD | Confluent Schema Registry compatibility check |
| Deprecated endpoints actually retire | Endpoints past their sunset date = 0 | CI script over a deprecation manifest |
Data
| Fitness function | Metric & threshold | Tooling |
|---|---|---|
| Migrations are backward compatible | Destructive DDL in a single deploy = 0 (expand/contract enforced) | squawk, migration linter |
| Every table has an owner and a retention policy | Untagged tables = 0 | Catalogue check |
| PII is labelled and encrypted | Unlabelled columns matching PII heuristics = 0 | Schema scanner |
| No long-running lock in a migration | Estimated lock time ≤ 1 s | squawk, explain-based check |
Observability
| Fitness function | Metric & threshold | Tooling |
|---|---|---|
| Every endpoint is traced | Handlers without a span = 0 | OpenTelemetry instrumentation audit |
| Every log line carries a correlation id | Sampled lines missing trace id = 0 | Log-pipeline assertion |
| Every service has an SLO and a burn alert | Services without both = 0 | Config check over the SLO repository |
| Dashboards are not orphaned | Dashboards referencing dead metrics = 0 | Scheduled metric-existence check |
Cost
| Fitness function | Metric & threshold | Tooling |
|---|---|---|
| Infrastructure cost delta per PR | Monthly delta ≥ $200 requires approval | Infracost in the PR comment |
| Cost per thousand requests | Within 10% of baseline | Billing export ÷ request count, weekly |
| LLM spend per request | ≤ $0.004 median; alert at 1.5x | Token accounting in the gateway |
| No idle expensive resources | Resources under 5% utilisation for 14 days = 0 | Scheduled cloud audit |
Maintainability
| Fitness function | Metric & threshold | Tooling |
|---|---|---|
| Changed-line coverage | ≥ 80% on the diff, not the repo | diff-cover, Codecov patch status |
| Complexity ceiling | Cyclomatic complexity ≤ 15 per function | eslint-complexity, radon, PMD |
| File size ceiling | ≤ 600 lines | Lint rule |
| Duplication | ≤ 3% and non-increasing | jscpd, PMD CPD |
| Build stays fast | CI wall time ≤ 12 min p95 | Pipeline metric with an alert |
| No flaky tests accumulate | Tests failing intermittently over 7 days = 0 | Test-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 function | Metric & threshold | Tooling |
|---|---|---|
| No WCAG violations on key routes | axe critical/serious = 0 | axe-core, Playwright + axe |
| Contrast ratio | ≥ 4.5:1 for body text | Automated token audit |
| Keyboard reachability | Interactive elements unreachable by keyboard = 0 | Playwright 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 function | Metric & threshold | Tooling |
|---|---|---|
| Eval suite pass rate | ≥ 92%, no regression on the golden set | Your eval harness, run in CI |
| Grounding / citation rate | Answers with a valid source ≥ 95% | Automated judge over the eval set |
| Tool-call schema validity | Malformed tool calls = 0 | Schema validation in the harness |
| Prompt token budget | System prompt + context ≤ 60% of the window | Static token count in CI |
| Latency budget | p95 time-to-first-token ≤ 900 ms | Load test against the deployed endpoint |
| Cost per successful task | ≤ target; regression fails the build | Eval harness with token accounting |
| Injection resistance | Known 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:
- Layer/dependency rule — stops the fastest form of erosion, static, minutes to run.
- Changed-line coverage — makes new code testable without relitigating the past.
- Dependency CVE + secret scanning — the cheapest security posture available.
- Bundle size or latency budget — whichever your users feel.
- 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
- Building Evolutionary Architectures (fitness function taxonomy) — Ford, Parsons, Kua, Sadalage (verified )
- ArchUnit — architecture rules as unit tests — ArchUnit (verified )
- Open Policy Agent / Conftest — policy as code — OPA (verified )
- Core Web Vitals thresholds — web.dev, Google (verified )
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
- ArticleMaking Architecture Decisions That Scale
- ArticleEngineering Metrics That Matter
- ArticleMeasuring Developer Productivity: DORA and SPACE
- ArticleLLM Evals: Building the Regression Net
- PathwayEngineering Excellence at Scale
- ReferenceThe Distributed Systems Failure Catalog
- ToolSLO & Error Budget Calculator