Tracing a formal specification into a thousand files of code
We put requirement identifiers in the code comments. It sounded like bureaucracy and turned out to be the most useful thing we did on the project.
Most software projects begin with a feature list. Somebody writes down what the system should do, everyone agrees it looks right, and then the list is never opened again. Six months later there is a working system and a document, and no reliable way to tell whether they describe the same thing.
On a recent learning platform we tried something different. The project started from a written software requirements specification, every requirement got an identifier, and each identifier is cited in the code comment where that requirement is satisfied.
// FR-REG-039: registration numbers are allocated in sequence
// and must never collide, even under concurrent submission.
const next = await allocateRegistrationNumber(tx, intakeId);It felt like bureaucracy for about a week. Then it started paying for itself, in ways we did not anticipate.
Disagreements become defects
The rule we adopted is short enough to remember: if the code and the specification disagree, one of them is a defect. Not a discussion, not a judgement call — a defect, which somebody has to fix.
This sounds pedantic. In practice it removes an entire category of argument. When a client says the system is behaving wrongly, the question is no longer whose memory of a meeting is correct. It is: which requirement covers this, and does the code cite it? Either the code is wrong, or the specification is wrong, and both are fixable.
Onboarding stops being oral history
The usual way a developer learns a codebase is by asking someone who already knows it. That works until the person who knows it leaves, which on a long project is a question of when rather than whether.
With traced requirements, a new developer can work backwards. They find the behaviour, read the citation, open the requirement, and understand not just what the code does but why anyone wanted it. That last part is what usually goes missing, and it is the part that stops a well-intentioned refactor from quietly removing something important.
Audits become a search
Institutional clients get audited. When somebody asks how the system enforces a particular access rule, the answer is a search rather than an investigation:
$ grep -rn "SEC-AUZ-002" src/
apps/api/src/auth/guards/role.guard.ts:24
apps/api/src/modules/admission/admission.service.ts:118
packages/shared/src/permissions.ts:41Three files, each with a comment explaining its part. That is a fifteen-minute answer to a question that would otherwise take a day and end in a hedge.
What it costs
Honesty matters more than advocacy here, so: it is not free.
- Writing a real specification before writing code adds time at the start, when pressure to show progress is highest.
- Identifiers have to be maintained. A requirement that gets split into two needs its citations updated, and nothing enforces that automatically.
- It only works if the specification is genuinely maintained. A stale document with identifiers is worse than no document, because it looks authoritative.
That last one is the real risk. Traceability is a commitment to keep two artefacts in agreement, and the moment you stop, the whole thing inverts from an asset into a liability.
When it is worth it
Not always. For a two-week marketing site it is obvious overkill. It earns its cost when at least one of these is true:
- 01The system will be audited, or operates somewhere regulated.
- 02It will outlive the team that built it — which is most institutional software.
- 03It will be handed to a different team, whether an in-house one or another vendor.
- 04The cost of a subtle behavioural regression is high.
We now do it by default on platform work, and we tell clients what it costs up front. Nobody has yet asked us to stop.