I lost a save cycle last week to a bug that I now think is one of the most under-discussed failure modes in configurable systems: a validation rule that was itself incompletely configured.
Not invalid data. An invalid rule.
The setup
If you have ever built or maintained a form builder, you know the shape. Forms are not code, they are data, arranged in a hierarchy that looks roughly like this:
Form
└── Tab
└── Section
└── Question
├── type (text, date, number, radio, multi-select, ...)
└── validations[]
A question carries a type and a list of validations. Text questions might accept MaximumLength, MinimumLength, Email, PhoneNumber, Required. List questions bind to a shared option set so that "County" means the same 254 choices everywhere it appears. Authors compose forms in a UI, hit save, and the whole tree is persisted as a draft. Publishing is a separate, explicit step.
This is a good architecture. It is also where the trap lives.
The trap
Most validations are a single flag. Required looks like one too. You add it to a question and move on.
Except Required was not a flag. Selecting it rendered a second dropdown, with two choices, something like Required and Semirequired - a genuinely useful distinction when a field must be answered before publication but may be left blank during intake.
That second dropdown had no default.
So the rule sat there, attached to the question, looking complete in the list of validations, carrying no answer to the question "required how?" I built out the rest of the form. Fifteen or so questions. Hit Save.
'Required' validation type is required
Inline, per question, for every question where I had added Required and not set the sub-type. The save was rejected. The error was accurate. It was just forty minutes late.
Why this class of bug happens
The rule had two states that the model could not distinguish:
- Not configured yet - the author is mid-edit, nothing is wrong
- Configured to nothing - the author has moved on, and this is now broken
Both serialize identically. A Required validation with a null sub-type. The only thing separating "in progress" from "broken" is whether the author intends to come back to it, and intent is not a column.
So the system deferred the judgment to the only moment it could be sure the author was finished: submit. That is a reasonable engineering decision and a poor authoring experience, and both things are true at once.
Three ways out
Make the invalid state unrepresentable. If Required always needs a strictness, then the rule is not Required plus a modifier, it is two distinct rules: Required and Semirequired. One dropdown, no sub-dropdown, no null state. Most "field A is mandatory when field B is set" configuration problems dissolve the moment you stop modelling the dependency and start enumerating the valid combinations.
Default to the safe option. If enumeration is genuinely awkward, pick a default. Strict Required is the conservative choice: it can only ever reject data that a laxer setting would have let through. An author who wanted Semirequired will notice. An author who wanted Required and got null will not.
Validate at the point of edit. If you must keep the null state, do not save it up for submit. The moment focus leaves a half-configured rule, mark it. A red dot on the question in the tree costs nothing and turns a forty-minute feedback loop into a four-second one.
Any one of these would have saved the cycle. The third is the cheapest to retrofit and the least satisfying, because it treats the symptom.
The part that generalises
Configuration is code without a compiler.
When behaviour lives in a database rather than a source file, everything the type system used to do for free becomes someone's explicit responsibility. A half-written if statement will not compile. A half-written validation rule saves cleanly, sits in a table, and waits.
The instinct that serves me best here is to ask, of any config schema: what is the set of rows this table can hold that no author would ever have meant? If that set is not empty, the schema is doing less work than it should, and the difference is being paid for at save time by whoever is holding the keyboard.
Worth an hour of design. Cheaper than forty minutes and a lost form.
Working notes from building form-driven systems. If you have fought the same fight in a different builder, I would like to hear how you modelled your way out.
Top comments (0)