Fitness Functions: Unit Tests for Your Architecture
You drew a layer rule and wrote it in a wiki page. It was violated eleven months ago and nobody noticed, because nothing was watching. A fitness function is a test for your architecture instead of your domain — and an architectural rule you cannot fail a build on is a wish.
Your architecture document says the presentation layer never touches persistence.
Someone imported the repository into a controller eleven months ago. It passed review — the diff was forty files and the import was line nine. It has been copied twice since, because the codebase now contains precedent.
Nothing failed. Nothing was watching.
You have a comprehensive test suite. Every one of those tests asserts something about your domain: that a discount applies, that an address validates, that an order totals correctly. Not one of them asserts anything about the shape of the system.
The definition, and the split it implies
An architectural fitness function is:
any mechanism that objectively assesses whether some architectural characteristic still holds.
Unpack it clause by clause, because each one is doing work:
- Any mechanism — a unit test, a monitor, a linter, a chaos experiment, a metrics query. Not a product you buy.
- Objectively — a number or a boolean. "The code feels layered" is not a fitness function.
- Still holds — it fails, loudly, at the moment the property stops being true.
- An architectural characteristic — not domain behaviour.
Which gives an unusually clean scoping rule:
Is any domain knowledge required to execute this test? Yes → a unit or acceptance test. No → a fitness function.
Validating the parts of a mailing address needs to know you handle addresses: a normal test. Asserting that no cycles exist between components, or that p99 latency holds at 5,000 concurrent users, requires no knowledge of whether you are an ecommerce site or an online game. That is a fitness function.
Same tooling. Same CI. Different target.
Three you can write this week
1. No cycles between components.
Cyclic dependencies (A→B→C→A) are the headline structural anti-pattern: no component in the loop can be reused, tested, or extracted without dragging the others along, and each new cycle takes the codebase one step closer to a big ball of mud.
The cause is mundane, which is exactly why a human process will not catch it. It is the editor's auto-import prompt — the one every programmer learns to dismiss reflexively, without reading which module it just wired in. Code review catches that a week late, if at all. A build catches it in ninety seconds:
// Fails on any cycle, and names the loop it found.
it('has no cycles between modules', async () => {
const cycles = findCycles(await buildImportGraph('src'));
expect(cycles).toEqual([]); // e.g. ['src/order → src/billing → src/order']
});
2. The dependency rule. Inner layers import nothing outward. This is the boundary you drew on the whiteboard, expressed as something that fails. In a TypeScript monorepo it can be as blunt as a scan of the import graph:
it('domain imports nothing from adapters or infrastructure', async () => {
const offenders = (await importsOf('src/domain')).filter(
(i) => i.startsWith('src/adapters') || i.startsWith('src/infrastructure'),
);
expect(offenders).toEqual([]); // names the violating file when it fails
});
3. A coupling budget. Afferent and efferent coupling are countable — fan-in and fan-out per component. Assert a ceiling on the components you care about. When it trips, you get a conversation at the moment of the change rather than an archaeology project in a year.
Start with these because they are structural, objective, and you already believe in them. That last part matters: enforce the rule you would genuinely revert a pull request for.
Why this beats the document
The honest reason architecture decays is not ignorance. It is that architectural decisions are important but not urgent, and that category loses to feature pressure every single sprint. Projects drown in urgency, the important-but-not-urgent slips quietly to one side, and the whole thing is booked as technical debt on the strength of a sentence everybody has said and nobody has honoured:
"We know this is bad, but we'll come back and fix it later."
A fitness function converts an intention into a constraint. The rule stops depending on whether the reviewer was paying attention at line nine of a forty-file diff.
The model to have in mind is the surgical checklist and the pre-flight checklist. Neither exists because the professionals involved are forgetful or do not know their job. They exist because when an expert performs the same task for the thousandth time, a skipped step feels exactly like a completed one. Fitness functions are that checklist, executed by the build instead of by the person least able to notice.
This is also the cheapest form of governance available. A failing build is a smaller, faster, less political conversation than a review board — and it happens at the moment of the mistake, addressed to the person who made it, while the context is still in their head.
The honest limits
Not every characteristic is automatable. Some are testable in CI — structure, cycles, dependency direction, bundle size. Some need production monitors, because they are runtime properties: dynamic coupling between services is observable only while the system is serving traffic, so its fitness functions have to run continuously rather than once per build. And some characteristics — a domain model's fidelity to the business, say — resist automation entirely. Say which is which rather than pretending the build covers everything.
Over-constraint is the failure mode. A suite of fitness functions encoding aspirations rather than commitments becomes the new bureaucracy — and worse, teaches the team that a red build is negotiable. Enforce rules you would actually block a merge for. Three real ones beat twenty aspirational ones.
They measure conformance, not correctness. A perfectly enforced bad architecture is still a bad architecture. Fitness functions keep the system matching the design; whether the design is right is a different conversation.
The takeaway
Pick the one architectural rule you would be most embarrassed to discover was violated six months ago.
Make it fail the build this week.
An architectural rule you cannot fail a build on is not a rule. It is a preference you have written down.