Design for the child with no attendance
A guardian wants one page that tells them how all three of their children are doing. Two are school students with tutors, timetables and homework. The third is sitting a nursing licence exam and has no attendance, no homework, and none of the figures the other two have.
The tempting design renders a row of stats per child. It works beautifully in the demo, where every child is well-behaved, and fails on every real family. Most of the work on the first version of this platform was refusing to build for the well-behaved case — and those refusals are now written into the rebuild as acceptance criteria.
Absent is a state, not an error
The family page treats a missing figure as information. It renders three children on two different products without forcing one shape onto both. It stays silent about movements under five points, because a small swing is noise, not news. It shows nobody else’s child anywhere — including on the group class two of the sisters share. And when a lesson is still waiting for its tutor’s sign-off, it says so, instead of quietly leaving it out of an attendance figure.
The money side follows the same rule. A tutor’s earning is held until the lesson is signed off, so “ready to be paid” reads zero when nothing is — the real state, not a bug. The finance role is refused everywhere that isn’t money, and that refusal is tested as carefully as what it can reach.
Seed the data that breaks things
The question bank includes eight question types, and the seed data deliberately contains the ones that break layouts: Arabic fill-in-the-blanks with English drug names and Latin numerals between the gaps. They’re the most reliable way to break a right-to-left layout, and I’d rather they broke it in a demo than in front of a student.
Turn a mystery into an instruction
In the first version, the exam engine was a separate service that the platform called with a scoped token. Adding a new ability to that token doesn’t widen a token already deployed, so every call to the newly gated route answers 403 until the token is reissued. Rather than surface a bare permission error, the client names the cause. That’s the difference between a mystery and an instruction.
Each test layer catches what the last one can’t
The first version’s Flutter companion app had a four-step verification ladder — analyse, test on the VM, test in a real browser, build the APK — and each step caught something the one before it couldn’t. The browser found a deserialisation failure the VM never saw, because the VM’s fake happened to match while the real API omitted a key.
And two bugs got through all four, found only by driving the running app. That’s the honest lesson, and it was written down in the repo rather than rediscovered: automated layers narrow the gap, and someone still has to use the thing.
The principle
Design for the real record, not the complete one. Treat missing as a state, seed the data that breaks you, make errors explain themselves — and then go and use what you built.