Back to work

Giant Reports by Brobd Solutions · 2026 · Product Engineer

Canteen management

A payment log rebuilt as a metered utility after the founder brought a better model than mine.

The problem

Ghanaian day schools feed children at a canteen and take small cash sums every day, some paid ahead weekly or termly. It is the most cash-intensive money in the school and the least recorded: an exercise book, a cash tin, and a reconciliation done from memory. The founder put it plainly. Schools get cheated at the canteen. Collectors under-remit, kitchen staff under-serve and take stock home, and because nobody can say how much cash should exist at the end of a day, nobody can say how much is missing.

I built the first version as a payment log, and it is the clearest illustration of the problem because it encoded it. The collector sets an amount at the top of the screen, taps Daily or Weekly against each child, and publishes. The number is typed by a person, the class roster is the denominator, and a child who is absent is indistinguishable from a child who owes money.

  1. 01

    The price of a meal depended on who was collecting

    Rates lived in a text field at the top of the collection screen. Whatever was typed became the charge, so the price of a meal depended on who was collecting and what they remembered.

  2. 02

    The class roster was the wrong denominator

    Collection was measured against everyone on the roll, so absent children, exempt children and children who brought lunch from home all read as debtors. Collection rates looked worse than they were and arrears accrued against families who owed nothing.

  3. 03

    Unpaid meant four different things

    A single status column collapsed did not eat, has not paid yet, is exempt, and is covered by a termly invoice into one word. The collector could not tell which rows needed action.

  4. 04

    A gap in the cash had nowhere to live

    When a class handed over less than the records said it took, either the day refused to close and stranded a whole school on one child's lost coin, or the difference quietly vanished.

Design thinking

Before any code, the thinking starts with brainstorming the problem space: who uses this, what can go wrong, what edge cases exist when money and attendance intersect. Every scenario that matters gets talked through, from a child arriving late to a guardian opting out mid-term to a collector who miscounts cash.

From there I build a canvas, a scratchpad for mapping out every screen and user flow before committing to code. Flows get rearranged and thrown away until the model holds.

Only once the canvas proves the logic does building in code start.

Screen zero of the layout canvas. Eligibility and recording as two separate acts, the seven outcomes a collector can record, and the rule the whole model rests on written in red: attendance never marks anyone paid, and never draws down a wallet.

What changed

Before

Version one. An Amounts bar the collector fills in before anything can be recorded, one Status column where every child reads Unpaid, and Recorded 0 of 8 measured against the whole class.

After

The rebuild. Rates read-only from policy, Expected to eat 44 with its derivation printed underneath, and Recorded 5 of 41 measured against those who can still be charged.

Everything the rebuild argues is visible in the two denominators. Version one counts the register: 0 of 8, where 8 is simply everyone on the roll. The rebuild counts the people actually due a meal and not yet settled: 5 of 41, with 3 not chargeable and 3 already paid ahead held separately. The second frame also splits settlement out from status, so cash, wallet and money paid in the parents app stop being the same word.

The daily loop

Collection is a routine done under time pressure with a queue of children waiting, so the whole design is judged on whether it survives being done quickly.

  1. 01

    Land on the collector home

    Cash taken today, money paid in the app, and students recorded, then every class as a card with its own running total. The first question a collector has is which class am I doing, so that is the whole screen.

  2. 02

    Open a class

    The roster, with the derivation at the top and every child already carrying a resolved status and a settlement method. Nothing here waits on the collector to work anything out, and a child on a wallet is asked only whether they ate.

  3. 03

    Record an outcome that is not a plain payment

    Daily, weekly and termly sit on the row itself. This modal is only for the outcomes that need an amount or a reason: part payment, credit, exempt, opt out. The product states the version one difference in its own words.

  4. 04

    Count the cash at day's end

    Handover is per class, and the system already knows what should be there. The bursar enters what actually is.

  5. 05

    Come up short

    A variance needs a reason before the day can close. Classes that have not been counted stay open rather than blocking everyone else.

Iterations made from feedback

The founder came to the demo two and a half weeks in with a sixteen-screen prototype of his own and a different mental model, and he was right. Canteen is a metered utility, not a payment log. One engine answers a single question per child per day: is this a chargeable meal? It resolves through the calendar, then school policy, then participation, then attendance. Cadence stops deciding whether a meal is chargeable and only decides when it settles.

The load-bearing decision inside the rebuild is that eligibility and recording are separate acts. The system resolves what should happen today. A person records what actually happened. Nothing ever settles itself, so there is no auto-deduction from a wallet and an attendance mark never counts as a payment.

The second is two separate ledgers. Feeding money never blends with school fees, so a child can be square on lunch and behind on tuition and both read correctly, and lunch arrears roll into next term as their own bill rather than disappearing into the fees invoice.

The roster mid-morning. Five decisions are visible on this one screen.
  1. 1

    Rates, read-only, from policy

    Why

    In version one this was an editable field at the top of the collection screen. Money should not be definable at the counter, so it moved to policy and comes back as a fact with a route to change it in the right place.

    What it changed

    The price of a meal stopped depending on who was collecting. The collection screen only records.

  2. 2

    Expected to eat, and its arithmetic

    Why

    The whole product in one number. Printing the derivation underneath, roll minus absent minus exempt minus opted out, means nobody has to trust it. Every input is data the platform already holds.

    What it changed

    Fake arrears stopped accruing against families who owed nothing, and the kitchen, the collector and the ledger all read the same figure.

  3. 3

    One status column, two kinds of truth

    Why

    System-resolved outcomes like absent and exempt sit in the same column as recorded ones like paid by cash. Splitting them into two columns tested worse: the collector reads down one column asking what still needs me.

    What it changed

    Unpaid stopped meaning four different things. A row now says whether it needs a person, and if not, why not.

  4. 4

    Arrears, kept apart from today

    Why

    What someone owes from previous days is a different question from what is due now, and merging them is how a collector ends up taking arrears money at the counter, which this phase deliberately does not do.

    What it changed

    Today stays a clean decision. Arrears are visible without becoming something to collect on the spot.

  5. 5

    Actions only where a decision is owed

    Why

    Four buttons with no primary among them, because cadence is genuinely unpredictable per child and per day. They appear only on rows still awaiting a decision.

    What it changed

    The eye goes to the rows that need work. A resolved row offers nothing to press, so there is nothing to press by mistake.

Tradeoffs

I threw away two and a half weeks of work in one meeting. That was only affordable because of how it was built, which is the point of the method rather than an aside.

Frictionless adoption went deliberately. Collection is impossible until the attendance register is marked, which means the canteen now depends on a completely different part of the school system. The founder wanted exactly that: if the school's cash depends on attendance being marked, supervision arrives on its own. It also means a teacher who has not marked the register can stop the canteen collecting, so that screen had to do more than refuse. It names the blocker, says who clears it, and offers a one-tap route to fix it.

Hard guarantees went too, in favour of reason and proceed. A short handover closes with a named reason instead of blocking, which accepts that some gaps get explained and never chased, in exchange for days that always close.

And the refund button went entirely, once it was clear refunds are a rare manual accounting event rather than a product flow.

The cost of the hard dependency, and the reason it is defensible. It names the blocker, explains why collection cannot start without the register, says who clears it and where, and offers a one-tap reminder. Without this screen the collector sees an empty table and blames the app.

Outcomes

Cash integrity and the platform leverage built on top of it landed together, because they are input and output of the same thing. Policy layering across school, grade level and student, collection by class and by search, cash handover with counting and reconciliation, the feeding ledger with a shortage holding account, arrears with guardian reminders, the audit log, and wallets with mobile money top-ups and shared family pots.

The metered model is now the direction for canteen across the product, and two new roles, collector and canteen supervisor, entered the permission model because of it.

Explained is not resolved. Fourteen cedis are still outstanding across three classes, each with a stated reason, because a reason closes the day but does not account for the money.
Arrears, with no money taken on this screen. The only outlet is a reminder to the guardian, one at a time from the row or in bulk from the header, so a collector can never quietly settle old debt at the counter.

Every pain point, answered

The price of a meal depended on who was collecting

Rates lived in a text field at the top of the collection screen. Whatever was typed became the charge, so the price of a meal depended on who was collecting and what they remembered.

Rates moved to policy and render read-only on the roster with a link to edit them where they belong. The collection screen stopped being a place where money is defined and became a place where it is recorded.

The class roster was the wrong denominator

Collection was measured against everyone on the roll, so absent children, exempt children and children who brought lunch from home all read as debtors. Collection rates looked worse than they were and arrears accrued against families who owed nothing.

The lead statistic became expected to eat, derived live as roll minus absent minus exempt minus opted out, with the arithmetic printed underneath it. Nobody types that number and nobody can.

Unpaid meant four different things

A single status column collapsed did not eat, has not paid yet, is exempt, and is covered by a termly invoice into one word. The collector could not tell which rows needed action.

Status became system-resolved and settlement-typed, so paid by wallet, paid by cash, paid in the parents' app, exempt on health grounds, absent from the register, opted out and on the school bill are all distinct. Only rows genuinely awaiting a decision carry buttons.

A gap in the cash had nowhere to live

When a class handed over less than the records said it took, either the day refused to close and stranded a whole school on one child's lost coin, or the difference quietly vanished.

Variance needs a named reason before the day closes, and the unresolved amount then sits in a shortage holding account, visible and outstanding, until an outcome accounts for it. Explained is not resolved.

The handover

A pull request is raised with the full prototype and detailed documentation covering the entire feature. Every screen, every state, every edge case is documented so a developer picking it up has no ambiguity about what was built or why.

The docs spell out limitations, what the prototype fakes versus what comes from real endpoints, and where the boundaries are between frontend state and backend responsibility. Open questions are listed plainly rather than guessed at, and next steps are scoped so the backend team knows exactly what to build first.

How it was built

This is the fullest expression of how I work. I ran the entire feature, product thinking and code, assisted daily by an agentic workflow, on a method I think of as design in code: the codebase is the design tool, a disposable branch is the artboard, and the output is a production ready prototype that communicates design intent to engineers.

On the playground branch, every surface carries a floating toolbar of state knobs, so any state, empty or partial or overdrawn or offline or six weeks into a term, is one click away without completing a flow. That is how I quickly iterate and accelerate my workflow and output. When a feature is ready, work is ported to a clean branch, productionized, and raised as a PR.

The one-day first version and the five-day rebuild are the proof. When building is that cheap, being overruled after a demo is a normal Tuesday, and the second version is better precisely because the first one existed to argue with.

Get to know more about my workflow

Thank you for reading all the way through. I appreciate the time. If this sounds like the kind of work you need, do get in touch. I’d love to hear what you’re building.

Get in touch

Next.js, TypeScript, Tailwind, React Query, react-hook-form