How do you keep per-field rules reviewable as a pupil record grows from twelve fields to two hundred?
answer
- the grid grows two ways
- classify the field, do not write a rule
- rules attach to the class
- unclassified resolves to the strictest class
basics
~20 sAttach rules to a small set of named sensitivity classes rather than to individual fields. Each field is assigned to exactly one class, so adding a field becomes a classification decision reviewed once instead of a new rule per role.
solid answer
~50 sA rule per field per role grows as roles times fields, and at two hundred fields nobody reads it, which means nobody reviews it either. Instead define a handful of **sensitivity classes** — say `open`, `academic`, `contact`, `pastoral`, `restricted` — and hold two artefacts: a map from every field to exactly one class, and a small grid of role against class giving read, write or neither. Adding a field is then a one-line classification, and the diff a reviewer reads is a field's class rather than a block of rules. The default carries the weight: a field the map does not mention must resolve to the strictest class, so a forgotten field fails closed and surfaces as a staff member reporting missing data. The tension to manage is exceptions — a field fitting no class either forces a new class, which re-opens every role's decision, or a recorded per-field exception.
code
json · 20 lines{
"defaultClass": "restricted",
"fieldClasses": {
"pupilId": "open",
"formGroup": "open",
"predictedGrade": "academic",
"attainmentBand": "academic",
"guardianPhone": "contact",
"guardianEmail": "contact",
"safeguardingFlag": "pastoral",
"safeguardingNote": "pastoral",
"freeSchoolMeals": "pastoral"
},
"grid": {
"classTeacher": { "open": "write", "academic": "write", "contact": "read", "pastoral": "none", "restricted": "none" },
"pastoralLead": { "open": "read", "academic": "read", "contact": "read", "pastoral": "write", "restricted": "none" },
"officeAdministrator": { "open": "write", "academic": "none", "contact": "write", "pastoral": "none", "restricted": "none" },
"headTeacher": { "open": "write", "academic": "read", "contact": "read", "pastoral": "write", "restricted": "read" }
}
}go deeper
The point to carry away is that rules are not written one field at a time. Fields are grouped into a few named sensitivity classes and the rules attach to the classes instead.
Be able to draw the two artefacts: a map from every field to exactly one class, and a small grid of role against class giving read, write or neither for each pair.
Show how the model survives change. Say what happens to a field nobody classified, and why the strict default is right even though it makes every new field look broken on deploy.
The judgement is the exception budget. A new class re-opens every role's decision and an exception does not, and once exceptions outnumber classes the classification has stopped describing the data.
## Why the matrix is the wrong artefact The naive field-level model writes one rule per role and field. A pupil record with 12 fields and 5 roles is 60 cells, which a person can read. The same record at 200 fields is 1,000 cells, and the interesting property of a 1,000-cell grid is not that it is large — it is that **no one reviews a change to it**. A new field arrives with five new cells in a pull request nobody can evaluate, because evaluating them means holding the whole grid in your head. The model has not become wrong; it has become unreviewable, which produces wrongness on a delay. The fix is to stop treating the field as the thing a rule is about. ## Classifying the field, not writing a rule Define a small number of named **sensitivity classes** and attach the rules to those. Two artefacts replace the grid: - A **classification map**: every field of the record type to exactly one class. One line per field, and the line is a judgement anyone who understands the data can make. - A **role-by-class grid**: small enough to fit on a screen, and therefore small enough to argue about. For a school's pupil records: | Class | Class teacher | Pastoral lead | Office administrator | Head | |---|---|---|---|---| | `open` | write | read | write | write | | `academic` | write | read | none | read | | `contact` | read | read | write | read | | `pastoral` | none | write | none | write | | `restricted` | none | none | none | read | Five classes and four roles is 20 cells that stay 20 cells as the record grows. Adding `pupilPremiumEligibility` is now a single question — *which class?* — asked of someone who knows what the field means, rather than four questions asked of someone who knows the grid. The property this buys is **reviewability in the diff**. A reviewer seeing `"pupilPremiumEligibility": "pastoral"` can judge it in isolation. A reviewer seeing four new rule rows cannot judge them without reconstructing the model. ## The default that makes a forgotten field safe The classification map will be incomplete, because a field can be added to the record type without the map being touched. So make the resolution **total**: a field the map does not mention resolves to the strictest class, which in the grid above is `restricted`. This matters more than the class design, because it decides how the model fails: 1. **Strict default.** The new field is invisible to almost everyone. Within days a head of year reports that a column is missing, someone classifies it, and the incident cost is a support ticket. 2. **Permissive default.** The new field is readable by everyone until somebody notices, and nobody notices, because nothing is missing from any screen. The incident cost is whoever eventually looks. The strict default is the one that produces noise rather than silence, and noise is the only failure mode you can act on. It is worth saying out loud that the strict default is mildly unpopular: it makes every new field look broken on first deploy. That is the price, and it is cheap. ## Exceptions, and how many you can afford Sooner or later a field fits no class. Two exits, with different costs: - **A new class** re-opens the decision for every role: five roles means five fresh judgements, and each is an opportunity to get one wrong. It pays when several fields will join the class, because the cost is then amortised. - **A per-field exception** leaves the grid untouched and costs one line, but exceptions are the thing that erodes the model — each is a rule attached to a field, which is exactly what the classes were built to avoid. The crossover is about population, not principle. Treat the exception count as a budget: a handful is maintenance, and when exceptions outnumber classes the classification has stopped describing the data and the class set needs redrawing. ## Detecting a wrong answer eighteen months later The failure this model is built to survive is silent: a field classified wrongly in year one, discovered in year three. Two things make it detectable without a person auditing anything. - **Enumerate the record type against the map.** The set of fields the type defines and the set the map names should be identical; a field in one and not the other is a question someone must answer, and it is visible as a list rather than inferred from behaviour. - **Make reclassification cheap and expected.** A field's class is not permanent — sensitivity changes when regulation changes or when a field starts carrying something it did not originally hold. Because a reclassification is one line touching one field, it can actually be done, which is the whole point of having built the model this way.
- A single new field genuinely fits no existing sensitivity class. New class, or a per-field exception?A new class re-opens the decision for every role, so it pays only when you expect several fields to join it. One field on its own takes a recorded exception with an owner and a review date. Track the exception count as a budget: a handful is ordinary maintenance, but once exceptions outnumber classes the class set has stopped describing the data and should be redrawn.
- How would you discover, a year later, that a new field was never classified?Make the resolution total so the failure is loud: an unclassified field resolves to the strictest class, and someone reports missing data within days rather than nothing happening for a year. Alongside that, enumerate the record type's fields against the classification map so a gap shows up as a list you can read, instead of being inferred from what people can or cannot see.
- What does migrating an existing product from per-field rules to classes actually cost?The rules themselves are cheap to move; the expensive part is that every existing field needs a class decision, and the current grid is evidence rather than an answer — some of those cells are already wrong. Expect to run both models against live traffic in a compare-only mode, resolve the disagreements one by one, and treat each disagreement as a finding rather than a migration bug.
A library does not write a borrowing rule per book. It shelves each book into a section — open shelves, reference, closed stack — and the borrowing rules attach to sections. A new acquisition is one shelving decision, not a new rule, and anything arriving unshelved waits in the closed stack.
saying these in an interview costs you the question
- One rule per field per role is fine; it is only configuration.
- A field with no classification should default to readable.
- Sensitivity classes are just roles wearing a different name.
- Adding a class is always cheaper than recording an exception.
- Once a field is classified it never needs reclassifying.
- Nobody adds fields often enough for the grid size to matter.