Architect
How-to guides
How-toNormative

The domain model

Entities and use cases in GraphQL SDL, derived from stories rather than nouns, run through six checks — and it stops for approval before anything downstream runs.

The most expensive artefact to get wrong, and the one where the failure is silent: a missing entity does not raise an error, it just quietly becomes a boolean on something else.

This is an approval gate. spec, roast and stories refuse an unapproved domain version.

It describes the target system, not the code. Nothing in it is "pending", "missing" or "to do". Where the client has not answered, model the shape that stays correct under every answer and move the question to a register — an optional link is a design decision, a hole is not. Auditing what exists is a different job with a different name.

Model at two levels

The product reference carries the cross-module model: important concepts, their relationships, and the single module that writes each fact. It answers who owns this?

Each module reference carries the field-level model for the facts that module owns. It answers what does this mean here?

One real-world thing may produce several records with different lifecycles. A reservation, a prepared handover and an invoice can describe one rental without forming one aggregate. Merge them only when one module truly owns every state transition. One writer per fact is the boundary test.

Start from stories, not from nouns

The obvious method — read the specification, extract the nouns — is what produces a model with holes, because it can only find what the specification already mentions. It organises; it does not discover.

Use Domain Storytelling. Tell concrete end-to-end stories, in the client's words, as actor → activity → work object:

The applicant submits a request to the registry. The registry assigns a mail number and passes the file to the Cabinet

Every noun in a true sentence about the business is a candidate entity, and every verb is a candidate command. Tell the awkward stories too, not just the happy one: discover → apply → assess → reassign → decide → pay → cancel. Four of the six failure modes below hide in the stories nobody tells.

Use EventStorming afterwards, and only where a lifecycle is genuinely complex — past-tense facts, the command that caused each, the aggregate that owns it. It is good at ordering what you have found. It does not find what nobody described: an absent event leaves no orphan.

The six checks

Run every one, on every candidate entity and every invariant. Each corresponds to a mistake that has actually been made and cost a rewrite.

1 — Is this a constant that should be data?

Can the client add, rename or retire this themselves, without a deployment, while old records keep the meaning they had?

If the answer is no and the thing is an organisation, a category, a threshold or a list, the model is wrong. See the configurability boundary.

2 — Does collapsing two dimensions turn AND into OR?

If I remove either dimension, does access disappear?

Two concepts that feel similar — a place and a service, a role and a team — are often an intersection. Merging them silently widens access. Render the access decision as a table before merging anything, and check both columns are load-bearing.

3 — Is this boolean hiding an entity?

Does this concept have its own identity, a counterparty, a state, a history, or a lifecycle that outlives the field?

A flag that answers "has X been requested?" cannot answer "of whom, when, is it still valid, what happened to it when the file moved?" Any Bool on an aggregate deserves this question.

4 — What makes the service available at all?

What opens it, to whom, from when, until when, and under what budget or quota?

Models routinely assume work simply arrives. Campaigns, calls, seasons, enrolment windows, quotas and eligibility periods are entities, and they are invisible from inside a process description because the process starts after they exist.

5 — Who is the counterparty in every transfer?

For every movement of money, information, authority or goods: who gives what to whom, under which identity?

A model with an applicant and an amount but no payee will pass every review until someone tries to pay. Follow each transfer to its far end and name what is there.

6 — What authorises this invariant?

Which legal text, client decision, or necessary derivation makes this true?

An invariant with no source is a decision nobody took. "Exactly one category per request" feels obvious and may be false. No source means it belongs in the open decisions, not in the model.

Write it in GraphQL SDL

Record the approved concepts and user-goal use cases with the canonical domain-model notation. That reference owns the GraphQL SDL, identifier, diagram and Cockburn-body conventions; this procedure owns how the model is discovered and approved.

The approval artefact

Approval must be possible in ten minutes without reading a hundred pages. Three screens, no more:

  1. The stories — actors, numbered activities, work objects. This is what gets checked against reality.
  2. The concept ledger — one row per entity: identity · lifecycle · relationships and cardinality · aggregate · administered or fixed · where the evidence comes from.
  3. The decision sheet — the six checks with their results, every unsourced invariant, and for each open question what the answer changes in behaviour. Not what it would add: if an open question can still add or remove an entity, the design is not finished and the artefact is not ready to be approved.

Then stop:

DOMAIN APPROVAL REQUIRED — version N

Nothing downstream runs on an unapproved domain. That is the whole point of putting it here: a wrong entity discovered after stories exist invalidates the requirements that trace to it, the stories that cite them, their acceptance tests and any migration already decided.

How this fails

The schema looks complete and is not. SDL has no syntax for an entity you never thought of. That is what the six checks are for, and skipping them because the schema compiles is the trap.

Approval becomes a rubber stamp. If the artefact cannot be read in ten minutes, it will be approved unread. Cut it until it can.

The model is derived from the specification. Then it inherits every hole the specification has, and the exercise proves nothing. Derive it from the stories, then check it against the specification — and where they disagree, that disagreement is the finding.

The document becomes an audit. The tell is a section called what is missing, a commented-out type, or a ⛔ next to an entity. Each one says "here is a hole someone else will fill", and each one is a decision dodged. Design it, or say in one line why it is out of scope — never leave it listed.

A field names something that does not exist. After the six checks, sweep the schema for references that point nowhere: a templateId: String, a requirementId: String, a free-text type. Each one is an entity the model implies and does not carry. The checks find what the stories omitted; this sweep finds what the schema itself already asked for.

On this page