A different domain
Domain-Driven Design taught us to model the business. We draw Order, Invoice, Customer; we name aggregates and their invariants; we insist on a ubiquitous language so the word "shipment" means one thing whether spoken by a developer or a warehouse manager. Done well, the model becomes the lingua franca of the team, and the code stops being a translation of the business — it is the business, expressed in types.
Requirements-as-code takes that discipline and points it at a different object. Not Order. Not Invoice. It models the justification of the software itself — why this system exists, and how we would know it is keeping its promises — as a bounded context in its own right.
That is the move worth slowing down for. We are used to modelling what the software is about. Here we model why the software is at all. The entities of this domain are not nouns from the business; they are obligations:
- a Requirement is a why — a promise the system makes and the reason it makes it;
- a Feature is the what that answers a why;
- an Acceptance Criterion is the how-we-measure that makes the why falsifiable;
- a Test is the proof that the measure holds;
- and the Evidence is the that-it-actually-runs — coverage, a passing gate, a metric in CI.
Give that domain a ubiquitous language and something quietly radical happens. The product manager and the developer stop negotiating in prose and start agreeing on names — the name of an acceptance criterion is a term both of them own, and the compiler holds them to it. (That collaboration is its own story; see Requirements Meet DDD for the Feature-as-bounded-context, AC-as-use-case mapping.) The domain being modelled is no longer the product. It is the link between the product and its reason to exist.
The rupture: the model does not stop at the concept
Here is where this departs from classic DDD, and it is the whole point.
Evans models down to the concept and then stops at the edge of the code. The aggregate diagram is gorgeous; whether the implementation actually honours it is a matter of faith, code review, and hope. The model and the running system remain two things we wish were aligned. DDD gives you a beautiful map and trusts you to keep it true to the territory.
Requirements-as-code refuses that gap. The model of justification is not finished when the diagram is clean. It is finished only when every why is connected, by a typed and compiled chain, to an evidence that runs in CI. The map is not allowed to describe a territory it cannot reach.
You can see this in the type of a Requirement itself. It is not a title and a paragraph. It carries its own descent toward proof as typed, mandatory fields:
abstract class Requirement<S extends RequirementStyle = RequirementStyle> {
abstract readonly statement: RequirementStatement; // the why, in a disciplined pattern
abstract readonly rationale: RequirementRationale; // the claim + its evidence
abstract readonly fitCriteria: readonly RequirementFitCriterion[]; // executable verifiables
abstract readonly verificationMethod: VerificationMethodsOf<S>;
// …
}abstract class Requirement<S extends RequirementStyle = RequirementStyle> {
abstract readonly statement: RequirementStatement; // the why, in a disciplined pattern
abstract readonly rationale: RequirementRationale; // the claim + its evidence
abstract readonly fitCriteria: readonly RequirementFitCriterion[]; // executable verifiables
abstract readonly verificationMethod: VerificationMethodsOf<S>;
// …
}A rationale is not free text — it is a claim backed by typed evidence: a metric, an incident, a study, a precedent. A fitCriterion is not a wish — it is one of a closed set of executable shapes:
type RequirementFitCriterion =
| { kind: 'unit-test'; describes: string; binds: readonly string[] }
| { kind: 'coverage-threshold'; metric: 'line' | 'branch' | 'mutation'; min: number; scope: string }
| { kind: 'quality-gate'; tool: string; rule: string }
| { kind: 'metric'; name: string; query: string; operator: '<' | '<=' | '=' | '>=' | '>'; threshold: number; window?: string }
// …type RequirementFitCriterion =
| { kind: 'unit-test'; describes: string; binds: readonly string[] }
| { kind: 'coverage-threshold'; metric: 'line' | 'branch' | 'mutation'; min: number; scope: string }
| { kind: 'quality-gate'; tool: string; rule: string }
| { kind: 'metric'; name: string; query: string; operator: '<' | '<=' | '=' | '>=' | '>'; threshold: number; window?: string }
// …Read that again with the thesis in mind. A coverage threshold scoped to a mutation metric. A query against a live metric with an operator and a threshold. These are not annotations about proof — they are the proof, named in the type system, sitting on the requirement that demands them. The why and the that-it-works are fields of the same object.
Invariants of the domain of justification
Once justification is a domain, it has invariants — and like any DDD aggregate, an invariant violated is an error, not a style preference:
- A Requirement with no Feature that satisfies it is an orphaned promise. The aggregate is broken: we have stated a why with no what.
- An Acceptance Criterion with no Test that verifies it is a wish wearing the costume of a measure. The model claims to be falsifiable and isn't.
- An approved Requirement that nothing implements is a specification-to-code gap the team has agreed to ignore.
In a prose world these are invisible — there is no compiler for a Confluence page. Here, the guard of the invariant is a command:
npx requirements compliance --strictnpx requirements compliance --strictcompliance --strict is not a linter for whitespace. It is the invariant guard of the domain of justification. It refuses the states where the model of intent is not closed all the way down to the proof: orphan promises, unverified measures, approved-but-unbuilt requirements. The build turns red not because the code is ugly, but because the justification has a hole in it.
Why this is the right shape
There is a philosophical commitment underneath, and it is worth naming. A system like this does not hand you a fixed library of "requirement types" to choose from. It hands you an ontology of the why that you instantiate for your own territory — your kinds, your statuses, your risk levels, your evidence (the next part shows the machinery). It is open to your domain and closed against drift: extensible at the vocabulary, rigid at the invariant. The framework never pretends to know what your promises are. It only insists that whatever they are, they be named, typed, and proven.
That is Domain-Driven Design carried one level up and one level deeper at once. Up, because the domain is the justification rather than the business. Deep, because the model is not allowed to stop at the concept — it descends until it touches the proof.
The next part draws the legend of that map: the typed language REQ ↔ FEAT ↔ AC ↔ TEST ↔ IMPL ↔ EVIDENCE, and why typing it is what makes it traversable by a machine.