FIELD NOTEPRODUCT

Modelling the domain before the schema, seen up close in public sector casework

What modelling the domain before the schema actually looked like from inside a regional housing authority.

FILED
READ
AUTHOR
REF

We spent six weeks inside a regional housing authority before we proposed anything. The brief said the problem was reporting. It wasn't reporting. Two days of watching caseworkers work through roughly 6,000 open cases made that obvious, and dashboards had nothing to do with it.

A schema is a set of answers. If you write it before you've settled the questions, you've encoded guesses into the hardest part of the system to change. Spend the extra week on the model and the schema takes an afternoon.

NOUNS ARE DECISIONS

Every entity you name is a claim that this thing exists independently, has a life of its own, and can be pointed at. Most arguments that look like technical arguments are really disagreements about whether something is an entity or an attribute, and they get much shorter once you say that out loud.

The test we use is lifecycle. Does it change on its own schedule? Does anybody care about its history? Does someone own it? Three yeses and it's an entity. Three noes and it's a column, and making it a table will cost you joins forever for no return.

GET THE VERBS TOO

Modelling usually stops at the nouns, which is half a model. The verbs are where the domain actually lives: the transitions, who's allowed to make them, what has to be true first. Write them down as a list of allowed moves before you write a single migration.

Do that and something useful falls out for free: the states nobody mentioned. Every domain has two or three legitimate states that don't appear in any documentation because everyone handles them by hand. Those are the ones that generate support tickets for the next five years.

The clearest thing we saw was how caseworkers handled a contested case record. On paper it's one step. In practice it's five, three of them over the phone, none of them written down. Which is why nobody could ever explain statutory turnaround to their director.

// Transitions first. The schema is downstream of this.
type Transition = {
  from: State
  to: State
  actor: Role
  requires: Guard[]
}

const TRANSITIONS: Transition[] = [
  { from: "draft",     to: "submitted", actor: "coordinator", requires: ["complete"] },
  { from: "submitted", to: "accepted",  actor: "reviewer",    requires: ["in_scope"] },
  { from: "submitted", to: "returned",  actor: "reviewer",    requires: ["reason_given"] },
  // The state nobody mentioned in the workshop, and the reason we do this first:
  { from: "accepted",  to: "withdrawn", actor: "coordinator", requires: ["before_cutoff"] },
]

THE WEEK THIS COSTS

It looks like a week of not building. In practice it's a week of finding out that two departments use the same word for different things, which you were going to find out anyway, just later, in production, after the migration.

We've never regretted the week. We have repeatedly regretted skipping it, and the regret always arrives in the same shape: a nullable column added in a hurry with a name like status_2.

Here it showed up as a queue nobody owned. Roughly 6,000 open cases went through it, and caseworkers had learned to check it twice a day because the alternative was a statutory clock was missed because the case had two owners. A better queue wasn't the answer. Making ownership a property of the case record was.

WHERE IT GOES WRONG

  • Generating the schema from a UI mock, which bakes this month's screen layout into next decade's data.
  • Modelling the nouns and leaving the transitions to whoever writes the controller.
  • Discovering in month four that two teams mean different things by the same word.
  • Adding a nullable status column in a hurry, then never being able to remove it.

Settle the nouns and the allowed moves before you write a migration.

WHAT WE TOOK AWAY

The work shipped and statutory turnaround moved, but the thing we're proudest of is smaller than the system: caseworkers stopped keeping a private spreadsheet. That's usually the honest signal that the model finally matches the job.

RELATED
SAME GROUND, DIFFERENT ANGLE
ALL TRANSMISSIONS