الدرس 2 من 4
Quality Attribute Scenarios and Fitness Functions
Turning "it should be maintainable" into something a build can check. This is how architecture stops degrading the moment you look away.
An architecture decays not through a single bad decision but through a hundred small ones, each locally reasonable, none of which anyone noticed. The defence is not vigilance — vigilance fails under deadline. The defence is automation: express the architectural property as something a machine checks on every commit.
Step one: write the scenario
A quality attribute scenario has six parts. The structure matters because each part is somewhere a vague requirement usually hides: without a stimulus you cannot test it, without an environment you do not know whether it applies under load, and without a response measure it is an opinion.
| Part | Example |
|---|---|
| Source | A user of the mobile application |
| Stimulus | Submits a booking search |
| Artefact | The availability search service |
| Environment | Normal operation, 500 concurrent searches |
| Response | Returns matching availability |
| Response measure | Within 300ms at p99, measured at the API gateway |
Step two: make it executable
A fitness function is any automated check that a system still exhibits an architectural characteristic. The term sounds exotic; most fitness functions are ten lines long and run in the same pipeline as the unit tests. Their value comes from running on every commit, not from sophistication.
// Runs in CI alongside the tests. Fails the build, not a report nobody reads.
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { globSync } from 'node:fs';
const FORBIDDEN = [/from '@prisma/, /from 'next/, /from 'react/, /@\/server\//];
describe('architecture: domain purity', () => {
it('domain layers import no framework or infrastructure', () => {
const violations = globSync('src/features/*/domain/**/*.ts')
.filter((file) => !file.endsWith('.test.ts'))
.flatMap((file) => {
const source = readFileSync(file, 'utf8');
return FORBIDDEN.filter((pattern) => pattern.test(source))
.map((pattern) => `${file} imports ${pattern.source}`);
});
// The message names the file, so a failure is actionable without digging.
expect(violations).toEqual([]);
});
});This platform runs exactly this class of check: `npm run boundaries` uses dependency-cruiser to fail the build if a domain layer imports React, Prisma or another slice. The rule is not a paragraph in a wiki that a new engineer may or may not read — it is a build failure with the offending import named, five seconds after the mistake.
Two ways to protect an architectural property
Documented and reviewed
The rule lives in a document; code review is expected to catch violations.
- Costs nothing to set up
- Handles nuance a mechanical check cannot
- Depends on reviewer attention, which is scarcest near a deadline
- New joiners violate it before they have read the document
- Erodes invisibly — nobody can tell you how many violations exist
Choose when: The property genuinely requires judgement, such as "aggregate boundaries should follow business invariants".
Executable fitness function
The rule is a check in CI that fails the build.
- Cannot be forgotten, skipped or deprioritised
- Teaches the rule at the moment it is broken
- Gives you a live count rather than a guess
- Survives every change of personnel
- Some effort to write, and it needs maintaining
- A badly-scoped check produces false failures and gets disabled
Choose when: The property can be expressed mechanically — layering, dependency direction, bundle size, p99 latency, public API compatibility.
- Layering: no import from domain into infrastructure. Checked by a dependency rule.
- Performance: p99 latency under 300ms at 500 concurrent users. Checked by a load test in the pipeline.
- Security: no endpoint reachable without passing through the authorisation helper. Checked by a route-table assertion.
- Compatibility: the public API schema is backwards-compatible with the last release. Checked by a schema diff.
- Coupling: no service reads another service's tables. Checked by database grants, which is stronger than any test.
- Bundle size: the first-load JavaScript budget for the marketing page. Checked by a size assertion on the build output.
In practice
Catching decay before anyone felt it
A team maintaining a modular monolith agreed that feature modules would communicate only through published interfaces, never by importing each other's internals. Everyone agreed. Nine months later, a routine review found 34 direct internal imports.
Constraints
- Seven engineers, three of whom joined after the rule was agreed
- Rule documented in the team wiki
- No mechanical enforcement
- Delivery pressure through two quarters
Decision
Add a dependency-cruiser rule to CI, fix the 34 violations over two sprints, and keep the rule permanently.
Why
Nobody had violated the rule deliberately. Two of the three new engineers had never read the wiki page, and the others had made locally reasonable choices at 6pm before a release. Documentation had failed in exactly the way documentation fails: silently, over months, with no signal.
What it cost
Two sprints of unplanned remediation, and some genuine friction where the rule was too strict and needed narrowing twice in the first month. The cost of the same drift discovered two years later — by which time the modules could not be separated at all — would have been a rewrite. The rule has failed the build 60 times since, each one a violation caught in five seconds instead of nine months.
Which architectural properties of a system you work on could be checked mechanically today, and are not?RevealHide
Almost every codebase has three or four available cheaply: dependency direction between layers, that no route bypasses the authorisation helper, that no module imports another module's internals, and that the production bundle has not silently doubled. The reason they are not checked is rarely difficulty — it is that nobody has framed the architectural rule as a testable assertion. That reframing is the skill; the code is usually ten lines.
Key takeaways
- Architecture decays through many small locally-reasonable decisions, not one bad one.
- A quality attribute scenario has six parts; the environment and response measure are where vague requirements hide.
- A fitness function is any automated check that an architectural property still holds.
- Enforced rules survive deadlines and personnel changes; documented rules do not.
- Start with narrow checks that always hold — a check that cries wolf will be deleted.