Leer en español · Lire en français · Auf Deutsch lesen
Section 11.3 of 11.8 · Advanced · 27 min read
Fitness functions and evolutionary architecture
Video lesson
5 min · narrated, with captionsListen only (audio)
Read the transcript
An evaluation is a snapshot. The day after it ends, every change can quietly undo a decision it relied on. In this lesson you'll learn how fitness functions check those decisions automatically and continuously.
A car's dashboard has warning lights for the things that must never go wrong unnoticed: oil pressure, engine temperature, brakes. You don't wait for the annual service to find out the engine overheated in March. A fitness function is a warning light for a software architecture. The team decides which qualities really matter, such as how fast an order is confirmed, and writes an automatic check for each. Those checks run every time the software changes. When one fails, the change is stopped or someone is alerted, while the problem is still small.
The term comes from the book Building Evolutionary Architectures, by Neal Ford and his co-authors. A fitness function is any mechanism that gives an objective assessment of an architectural characteristic. Any mechanism: a unit test, a load test, a monitor, a chaos experiment, even a scheduled manual review. Objective: the result mustn't depend on who runs it. A feeling that the code is well structured doesn't count. And a metric describes, but a fitness function decides. Breaking one forces a conscious choice: fix the code, or change the rule and record why.
Here's the anatomy of one. Start with a characteristic, such as order latency, and a way to measure it: the ninety fifth and ninety ninth percentiles, at a hundred orders a second. The threshold comes straight from the scenario's response measure: one and a half seconds, and three seconds. The check is a load test in the pipeline, and the action, when the threshold is broken, is to fail the build. A fitness function should fail loudly, not fill a dashboard nobody reads. Underneath, it traces back to its source scenario. And it has an owner, a review date, and a rationale recorded in an Architecture Decision Record, or ADR.
Fitness functions run all along the delivery pipeline. Atomic checks test one characteristic; holistic checks test several together. On every build come cheap, fast, atomic checks: dependency rules, cycle checks, complexity limits, and dependencies with known vulnerabilities. In the test stage come slower, holistic checks: a load test with percentile thresholds, schema and contract checks, and a failover test. At the canary, the new version's error rate and latency are compared with the old version's. And in production, continual checks run all the time: monitors on service level objectives, or SLOs, synthetic orders, chaos experiments and cost per order. Failures and drift feed back as new work, and often as new fitness functions.
Now the platform turns three scenarios into fitness functions. First: at a hundred orders a second, ninety five per cent of orders are confirmed within one and a half seconds, and ninety nine per cent within three. Fitness function one is a load test against a production-like environment, nightly and before every release. It fails if either percentile is over its threshold, or if more than nought point one per cent of requests fail. The one and a half seconds is also split into budgets for the network, the gateway, ordering, pricing and database writes, leaving five hundred and fifty milliseconds of headroom. Each service tests its own budget, so a regression is caught where it's caused. Scenario two is checkout uptime. Each week in staging, a chaos experiment makes the payment provider hang. Orders must still be accepted as pending within ten seconds, and all must settle after recovery. Scenario three: a new point of sale vendor should take five days of one person's work, by adding one adapter. A build rule stops the restaurant and ordering code importing any vendor's software kit, so the change stays local. Each one is recorded with its source scenario, its threshold, its owner, the ADR that explains it, and a review date.
Fitness functions are half of what those authors call evolutionary architecture: guided, incremental change across multiple dimensions. Small deployable parts and fast pipelines give the increments. Fitness functions give the guidance. Start small. Pick the three to five characteristics the utility tree rates highest, and write one reliable check for each. A dozen trusted checks are worth more than a hundred that fail at random. For an existing system, use a ratchet. Record today's violations as a baseline, fail only on new ones, and tighten the baseline as the debt is paid down. And keep the pipeline fast: atomic checks in every build, and heavy holistic checks, such as ten minute load tests, nightly or before release.
The full section covers the dimensions of fitness functions and the code for all three checks. Next, in Section eleven point four, we look at governance in agile organisations. See you there.
What you need to know
- An architectural fitness function is any mechanism that gives an objective assessment of an architectural characteristic, ideally automated and traceable to a quality attribute scenario whose response measure becomes its threshold.
- Fitness functions are classified as atomic or holistic, triggered, continual or temporal, static or dynamic, automated or manual, and intentional or emergent; a healthy set mixes them.
- Typical examples are dependency rules for structure, load tests and latency budgets for performance, vulnerability scans for security, chaos experiments for resilience and schema compatibility checks for data.
- Evolutionary architecture (Ford, Parsons, Kua and Sadalage) supports guided, incremental change across multiple dimensions; fitness functions provide the guidance, and small deployable units and pipelines provide the increments.
- Each fitness function needs an owner, a recorded rationale and a threshold, and a failure forces a decision: fix the code or change the rule and record why.
- Start with a few reliable checks for the highest-priority characteristics, use baselines to ratchet existing systems, and keep slow holistic checks out of every build.
From one-off evaluation to continuous checking
An evaluation such as the ATAM (Section 11.1) is a snapshot. The day after it ends, developers start changing the system, and every change can quietly undo a decision the evaluation relied on: a convenient import across a module boundary, an extra synchronous call on the order path, a library with a known vulnerability. Section 1.7 described the result: erosion, when the architecture is violated, and drift, when it is ignored. Periodic reviews catch some of this, months late. The idea in this section is to check the most important architectural decisions automatically and continuously, in the same way that unit tests check behaviour.
The term comes from Neal Ford, Rebecca Parsons and Patrick Kua's Building Evolutionary Architectures (O'Reilly, 2017), whose second edition, with Pramod Sadalage, appeared in November 2022. They borrowed it from evolutionary computing, where a fitness function scores how close a candidate solution is to the goal so that the algorithm can keep the better variants. In architecture, the goal is the set of characteristics the system must keep while everything else changes.
What a fitness function is
The second edition defines an architectural fitness function as any mechanism that provides an objective integrity assessment of some architectural characteristic or characteristics. Three words in that definition do the work. Any mechanism: a unit test, a static-analysis rule, a load test, a monitor, a chaos experiment or even a scheduled manual review all qualify. Objective: the result must not depend on who runs it; "the code feels well structured" is not a fitness function, "no package in the domain layer imports the web framework" is. Architectural characteristic: the check protects a quality the architecture exists to deliver (performance, availability, modifiability, security, data compatibility), not a business rule, which is the job of ordinary tests.
In practice a useful fitness function has a few more properties. It is traceable to a requirement, ideally a quality attribute scenario whose response measure becomes its threshold (Section 2.3). It has an owner who decides what happens when it fails. It fails loudly, by breaking a build or paging someone, rather than populating a dashboard nobody reads. And its threshold and the reason for it are recorded, usually in an ADR, so that a later team can tell the difference between a rule that protects something important and an arbitrary number.
Section 3.8 drew the important distinction with metrics: a metric describes, a fitness function decides. A coupling metric on a dashboard tells you things are getting worse; a fitness function encodes a decision the team has already made, so breaking it forces a conscious choice: fix the code, or change the rule and record why.
Anatomy of a fitness function
Categories of fitness function
Ford and his co-authors classify fitness functions along several independent dimensions. The classification is useful less as taxonomy than as a design prompt: asking "should this be atomic or holistic, triggered or continual?" often reveals a better check than the first one that came to mind.
Dimensions of fitness functions
| Dimension | Options | What it means | Example |
|---|---|---|---|
| Scope | Atomic or holistic | Atomic checks one characteristic in one context; holistic checks several characteristics interacting in a shared context | Atomic: no cycles between packages. Holistic: order latency measured with encryption, audit logging and caching all switched on |
| When it runs | Triggered, continual or temporal | Triggered runs on an event such as a commit, build or deployment; continual runs all the time against the live system; temporal fires after time passes, as a reminder to revisit something | Triggered: dependency scan on every build. Continual: SLO burn-rate alert in production. Temporal: fail the build when a pinned framework version is more than 12 months old |
| Kind of result | Static or dynamic | Static compares against a fixed threshold; dynamic adjusts the threshold to context, such as scale | Static: p95 under 1.5 s. Dynamic: p95 may rise to 2 s above 150 orders/s, as agreed for extreme peaks |
| How it is run | Automated or manual | Automated is preferred; some characteristics, such as legal or usability requirements, still need people, ideally on a schedule | Manual: quarterly accessibility review against WCAG 2.2 AA |
| Planning | Intentional or emergent | Intentional checks are designed up front from known requirements; emergent ones are added as the team learns what matters | Emergent: a check on consumer lag added after an incident |
Holistic checks deserve attention because qualities interact: security controls add latency, caching complicates data freshness, and an atomic test of each in isolation can pass while the combination fails. Continual checks matter because some characteristics only exist in production: real user latency, real failure modes, real cost. A healthy portfolio combines fast, atomic, triggered checks in the build with a smaller number of slower holistic checks before release and continual checks in production.
Where fitness functions run
Fitness functions across the qualities
Most teams start with structure, because structural checks are cheap, fast and deterministic, then add performance, security, resilience and data checks as the pipeline matures. The table gives typical examples; tools change, the patterns do not.
Examples by quality
| Quality | Fitness function | Kind | Typical tooling |
|---|---|---|---|
| Structure | Layer and dependency rules, no cycles, modules use each other only through public APIs (Section 3.4) | Atomic, triggered, static | ArchUnit, import-linter, dependency-cruiser, Spring Modulith verification |
| Performance | End-to-end load test with percentile thresholds; per-service latency budgets in each service's own pipeline | Holistic, triggered | k6, Gatling, JMeter, Locust; thresholds that fail the job |
| Security | Fail the build on dependencies with known high or critical vulnerabilities; no secrets in code; container images only from the approved registry | Atomic, triggered | pip-audit, npm audit, OWASP Dependency-Check, Trivy, secret scanners; policy as code (Section 11.4) |
| Resilience | Chaos experiments that kill instances or slow dependencies and check that the steady state holds (Section 9.8) | Holistic, triggered or continual | Toxiproxy, Chaos Toolkit, cloud fault-injection services |
| Data | Schema compatibility checks on events and APIs; migrations rehearsed on production-sized data (Section 7.7) | Atomic, triggered | Schema registry compatibility checks, buf breaking, consumer-driven contract tests |
| Operability and cost | Every service exposes health checks and standard metrics; cost per order stays below a target | Atomic or continual | Pipeline checks on service templates; cost dashboards with alerts |
Evolutionary architecture and incremental change
Fitness functions are one half of what Ford, Parsons, Kua and Sadalage call evolutionary architecture: an architecture that supports guided, incremental change across multiple dimensions. Each part of the phrase is a design requirement.
Incremental change has a development side and an operational side. Developers must be able to make small changes safely, which depends on modularity and appropriate coupling (Chapter 3) and on keeping each independently deployable part, the architecture quantum of Section 5.1, small enough to change without coordinating with others. Operations must be able to release small changes frequently and safely, which depends on a deployment pipeline, progressive rollout and fast rollback (Section 10.5). Guided is where fitness functions come in: they define what "fit" means and stop increments from degrading it. Multiple dimensions is a reminder that architecture is not only code structure: data, security, operations, team structure and cost all evolve and all need protecting.
The approach changes how architects think about the future. Instead of predicting every requirement and building flexibility for it in advance, which Section 3.8 showed is expensive and often wrong, the team keeps the system easy to change and protects only the characteristics that matter, deferring other decisions to the last responsible moment (Section 1.6). Critics point out real limits. Not every quality can be checked automatically or cheaply; badly chosen thresholds create noise that teams learn to ignore; and some decisions, such as a database or a cloud provider, remain one-way doors however good the pipeline is. Evolutionary architecture is a discipline for keeping the reversible parts reversible and the important parts protected, not a promise that everything can be changed later.
Worked example
Worked example: three Chapter 2 scenarios as fitness functions
Section 2.8 built a utility tree for the food-delivery platform. The team turns three of its leaves into fitness functions, one triggered and holistic, one a chaos experiment, one structural, after the ATAM in Section 11.1 led to asynchronous payment authorisation.
| Scenario 1: order latency (H,H) | At the Friday peak of 100 orders/s, 95% of order confirmations return within 1.5 s and 99% within 3 s |
|---|---|
| Fitness function 1 | Holistic, triggered: a k6 load test against a production-like environment, nightly and before every release, failing if p95 or p99 exceeds the threshold or more than 0.1% of requests fail. Backed by continual production monitoring of the same percentiles from real traffic |
| Latency budgets | The 1.5 s is split into budgets: mobile network and TLS 400 ms, gateway 50 ms, ordering 250 ms, pricing 100 ms, database writes 150 ms, leaving 550 ms of headroom now that card authorisation is off the path. Each service's pipeline runs an atomic component load test against its own budget, so a regression is caught in the service that caused it |
| Scenario 2: checkout uptime (H,H) | When the payment provider times out during the peak, orders are accepted and queued, no confirmed order is lost, and customers are told within 10 s |
| Fitness function 2 | Holistic chaos experiment, weekly in staging and quarterly as a production game day with a small blast radius: the provider is made to hang; orders must be accepted as pending within 10 s and all must settle after recovery. Owner: ordering team |
| Scenario 3: new POS vendor (M,L) | A new restaurant point-of-sale vendor is integrated in 5 person-days by adding one adapter |
| Fitness function 3 | Atomic, triggered, static: an import-linter contract forbids the restaurant domain and API modules and the ordering package from importing a vendor SDK, and a second contract stops adapters importing each other, so the change stays local. Supplemented by a manual check: the effort of each real vendor integration is recorded and compared with the 5-day target |
| Recorded with each | Source scenario, threshold, owner, the ADR that explains it, and a review date |
Each response measure became a threshold without reinterpretation, which is the payoff of writing scenarios well in the first place.
The modifiability scenario cannot be fully automated: the structural rule protects the conditions for a 5-day change, and the manual measurement checks the outcome.
Latency budgets turn one holistic number into atomic checks that tell the right team about a regression.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
friday_peak: { executor: 'constant-arrival-rate', rate: 100, timeUnit: '1s',
duration: '10m', preAllocatedVUs: 300 }, // 100 orders per second
},
thresholds: { // the scenario's response measure, as pass/fail rules
'http_req_duration{name:place_order}': ['p(95)<1500', 'p(99)<3000'],
'http_req_failed{name:place_order}': ['rate<0.001'],
},
};
const order = { restaurantId: 'r-42', items: [{ sku: 'pad-thai', qty: 1 }] };
export default function () {
const res = http.post(`${__ENV.BASE_URL}/orders`, JSON.stringify(order), {
headers: { 'Content-Type': 'application/json' }, tags: { name: 'place_order' } });
check(res, { 'order accepted': (r) => r.status === 201 || r.status === 202 });
}# Fitness function: checkout keeps accepting orders when payments hang (illustrative)
# Runs weekly in staging; toxiproxy, api and ledger are test fixtures.
def test_orders_survive_payment_provider_outage(toxiproxy, api, ledger):
toxiproxy.add_latency("payment-provider", latency_ms=30_000) # provider hangs
placed = [api.place_order(sample_order()) for _ in range(200)]
assert all(r.status in (201, 202) for r in placed) # still accepted
assert all(r.json()["state"] == "PENDING_PAYMENT" for r in placed)
assert max(r.elapsed_seconds for r in placed) < 10 # told within 10 s
toxiproxy.remove_toxics("payment-provider") # provider recovers
ledger.wait_until_settled(timeout_seconds=300)
assert ledger.lost_orders(placed) == [] # none lost# .importlinter: only the POS adapters may use vendor SDKs
[importlinter]
root_package = fooddelivery
include_external_packages = True
[importlinter:contract:pos-vendors-isolated]
name = Vendor POS SDKs are used only inside the POS adapters
type = forbidden
source_modules =
fooddelivery.restaurant.domain
fooddelivery.restaurant.api
fooddelivery.ordering
forbidden_modules =
tillpro_sdk
kitchenlink
[importlinter:contract:adapters-independent]
name = POS adapters do not import each other
type = independence
modules =
fooddelivery.restaurant.pos_adapters.tillpro
fooddelivery.restaurant.pos_adapters.kitchenlinkData compatibility is protected in the same way. Before a change to the OrderPlaced event can merge, the pipeline asks the schema registry whether the new schema is compatible with the latest registered version under the subject's compatibility rule, and fails if it is not:
# Data fitness function: block the merge if the new event schema would break consumers
curl -s -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data @build/order-placed-v7.request.json \
"$SCHEMA_REGISTRY_URL/compatibility/subjects/order-placed-value/versions/latest" \
| jq -e '.is_compatible == true'Designing fitness functions that last
Start small and important. Pick the three to five characteristics that the utility tree rates highest, write one fitness function for each, and make sure they are fast and reliable before adding more. A dozen trusted checks are worth more than a hundred that fail at random.
Use ratchets for existing systems. When a codebase already violates a rule, record the current violations as a baseline and fail only on new ones; tighten the baseline as the team pays the debt down. The same works for performance: fail on regressions of more than an agreed margin against the last release while working towards the scenario's target.
Make thresholds explicit and owned. Keep thresholds in version-controlled configuration next to a reference to the ADR that justifies them, with a named owner. When a fitness function fails, the owner decides between fixing the code and changing the rule, and the decision is recorded. Treat a fitness function that is frequently overridden as a finding: either the rule is wrong or the architecture is under real pressure.
Keep the pipeline fast. Atomic checks belong in every build; heavy holistic checks such as ten-minute load tests run nightly, before release or on the release candidate. Continual checks in production need the same care as any alert: tied to user impact, with a runbook, and silent when nothing is wrong.
Questions people ask
Are fitness functions just tests?
Many are implemented as tests, but their subject is different. Ordinary tests check that features behave correctly; fitness functions check architectural characteristics such as structure, latency, resilience or compatibility. Some, such as production monitors and manual reviews, are not tests at all.
Who writes fitness functions: architects or developers?
Both. Architects usually identify the characteristics and thresholds and often write the first checks, which is a good way to stay hands-on (Section 1.4). Teams own and maintain them like any other code.
How do fitness functions relate to SLOs?
An SLO with an alert on its error budget is a continual, holistic fitness function for availability or latency in production (Section 9.5). Pipeline fitness functions try to catch the same regressions before they reach users.
Check your understanding
What is the difference between a metric and a fitness function?
A metric describes a property; a fitness function encodes a decision with a threshold and an action, so breaking it fails a build or raises an alert and forces a conscious choice.
Give one example each of an atomic and a holistic fitness function.
Atomic: a rule forbidding imports from the web layer into the domain. Holistic: a load test measuring order latency with security, logging and caching all active.
What does a latency budget add to an end-to-end latency fitness function?
It splits the end-to-end target into per-service limits that each service checks in its own pipeline, so a regression is detected in the service that caused it.
What are the three parts of the definition of evolutionary architecture?
It supports guided (by fitness functions), incremental (small, safe changes and releases) change across multiple dimensions (code, data, security, operations, teams and more).
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “Which three fitness functions should we write first for a service that must confirm orders quickly and survive a payment provider outage?”