Engineering Journal · ENGINEERING · May 9, 2023 · 16 min read
"Add an Approval" Is Never Just a Boolean
The ticket says add a tick box. The domain says who may act, what happens if they are away, whether yesterday can be rewritten, and who lives with the record.
By Golam Sorwar, Tech Lead and Full Stack Engineer in Dublin.
The ticket is almost always short. Add an approval. Add a check. Make someone sign off. It arrives as if the work is a column and a button. I have learned to treat that sentence as a compressed file. If you unzip it, you find absence, reversal, history, money, and a person who will be on leave the week it matters.
I am not talking about ceremony. I am talking about the moment a product that already has users is asked to insert a human gate into a path that used to complete. In a college operations system that path might be a letter, a refund, a room change, a status that unlocks the next office. In an earlier back-office it was an order or a school record. The nouns change. The compression does not.
This is an article about requirements that sound like UI and are actually law.
Context
Operational products collect these requests because the organisation is already making the judgement by hand. Someone emails. Someone writes a note. Someone changes a status they were not supposed to change, then tells a colleague. The software is asked to "add an approval" when that informal gate has become too risky or too tiring to leave in chat.
The team hearing the request is usually small. That matters. There is no analyst whose only job is to expand the sentence. If engineering accepts the boolean, the boolean ships. The real rules arrive later as exceptions, and exceptions are how you grow a second system beside the first.
I have sat on both sides of this. As the person who wanted to finish the ticket. As the person who later had to explain why an approved record could not be undone, or why it was undone too easily.
The word shows up everywhere: refunds, letters, discounts, room changes, a status that unlocks the next office. If you treat each as a new flag, you will have six booleans and still no absence path. The questions repeat. That is a clue that you are looking at a type of work, not a one-off control.
The problem
The stated problem is missing control. Anyone can complete the action. We need a responsible person to say yes.
The actual problem is that "approval" is not one fact. It is a set of questions the organisation has usually not answered in the same room. Who is allowed to approve this class of thing. What happens if they are absent. Can two people approve, and if they disagree, which one wins. Can approval be reversed after a side effect — a payment, an email, a status another department already used. Do historical records need to look approved even if the rule did not exist then. What should an administrator be allowed to bypass, and how is that bypass recorded.
If you skip those questions, you do not skip the work. You schedule it for production, where a staff member will invent the answer with a workaround.
The tempting solution
Most of us reach for a flag. approved_at, approved_by, a policy that hides the next button. Maybe a notification. It demos well. The stakeholder can see the gate. The pull request looks like a feature.
If you are slightly more careful, you add a role check. Only finance. Only admissions. Only the assigned manager. That still fits in an afternoon if you do not ask what "assigned" means when the assignee has left.
Why that is not enough
A boolean cannot represent absence. The approver is on leave. The work is stuck, so someone with a broader admin role completes it, and now you have taught the organisation that the gate is optional for people who know where the admin screen is.
A boolean cannot represent reversal cleanly. If the side effect has already left the building — an invoice, a message, a downstream status — "unapprove" is not the inverse of a tick. It is a new business event with its own audience.
A boolean cannot represent history. Old rows have no approver. Reports will lie or fail. Someone will ask you to backfill. Backfill without a rule is fiction.
And a boolean cannot represent the thing that usually matters most: the action after approval. Approval is rarely the product. It is a door. If you model the door and not the room, you will approve things into a state the rest of the system does not understand.
Options
Keep the informal process and refuse the ticket. Advantage: you do not encode a half-rule. Disadvantage: the organisation already asked for software because the informal process is failing. Sometimes the honest answer is still no, until they can name the rule.
Ship the boolean and promise to "harden it later." Advantage: speed. Disadvantage: later never has a stakeholder. The boolean becomes the contract.
Model a small state machine: requested, approved, rejected, withdrawn, superseded, with actors and reasons, and treat side effects as listeners of a transition, not of a page submit. Advantage: you can talk about absence and reversal as states. Disadvantage: more to build and to teach. Risk: you over-model a one-person tick that really is a tick.
Put the workflow in a generic engine. Advantage: flexibility. Disadvantage: you have invented a programming language for staff, and the next change becomes configuration archaeology. I am wary of this unless the organisation already lives in workflow tools.
Trade-offs
A real approval model is slower than a flag and more honest than a meeting. You spend time in questions that feel like they are not engineering. They are the engineering. The code is the cheap part.
You also give up a certain kind of flexibility. Once states exist, people will ask for a new one every term. That is the cost of making the process visible. Invisible process is flexible because nobody can audit it.
What you should not trade away is history. If the record cannot say who decided, when, and whether a later reversal happened, you have built a decoration.
Decision
I treat "add an approval" as a design conversation with a written outcome before a migration. The outcome has to name the actor, the absence path, the reversibility rule, and what is allowed to happen after the transition. If those cannot be named, the ticket is not ready. That is a more useful no than "we are busy."
When they can be named, I want an explicit transition, not a lone boolean, even if the first version only has two states. A transition can grow a reason and a second actor. A boolean can only grow regret.
If the organisation cannot name the approver, I ask who they phone today. That person is the actor, even if the org chart says otherwise. Software that implements the chart and not the phone tree will be walked around by the phone tree.
Implementation / Thinking process
I start on paper, not in a migration. List the happy path. List the away path. List the "we were wrong" path. List the records that already exist. If any of those is a shrug, stop.
In the application, the write that matters is the transition. Controllers may collect the click. They should not be the only place the rule lives. Jobs and admin tools will otherwise invent a second door.
Side effects wait for the transition they actually depend on. If an email should not go until approval, the email listens to approved, not to created. If a payment already happened, approval cannot pretend to be a precondition you can insert after the fact without a separate reconciliation story.
Permissions follow the workflow, not the other way around. "Is admin" is not a substitute for "may approve this class of record." Admin bypass, if it exists, is a recorded event with a reason. Otherwise you have two products: the official one and the one people with the wider role use on Fridays.
Notifications are part of the model. An approval that nobody hears about is a stuck state with a UI. I do not need a real-time stack for this. I need a reliable "you have work" that does not share a queue with a bulk campaign.
Old records need a policy in writing: grandfather as unapproved-but-grandfathered, or leave them outside the new reports, or run a one-time review. What I do not want is a silent UPDATE that paints the past. That is fiction with a timestamp.
Failure modes
The most common failure is the holiday. The only approver is away, the work is urgent, and someone "just this once" uses a power you left lying around. You will not hear about it as a bug. You will hear about it as a process.
The second is double approval without a rule. Two people click. The system accepts both or rejects the second opaquely. Staff lose trust in the screen.
The third is rewriting the past. A report needs every historical row to look compliant, so someone updates old records to approved with a null actor. That is how you destroy the only evidence you had.
The fourth is notifying the wrong person, or nobody, and concluding that "approvals do not work" when the model is fine and the attention path is not.
Operational consequences
Once the gate exists, the organisation will route work to it. Queues will form. Someone will ask for a dashboard of pending approvals. That dashboard is not scope creep. It is the product you implied when you accepted the word.
Support will also start speaking in the new language. "It is waiting for approval" becomes a status they tell students or customers. If your states are sloppy, that sentence will be a lie, and you will have exported the lie.
There is a reporting consequence as well. Once the word exists, someone will ask for a count of approved versus pending, then for a time-to-approve, then for a list of people who sit on work. Those numbers will be used in meetings. If your model cannot distinguish "waiting for a person" from "waiting for a side effect," you will publish a metric that punishes the wrong office. The boolean cannot save you there. The states can, if you named them.
Lessons
Product language compresses domain. Engineering's job is to decompress it in public, early, while the cost of a question is a meeting and not a data repair.
A flag is a valid implementation of a rule that has been fully stated. It is a poor implementation of a rule that has not.
What I would do differently today
I would refuse to estimate in hours until the four questions have answers: who, absence, reversal, history. I used to estimate the button and then discover the state machine in review. That wastes the only cheap moment.
I would also write the absence path into the first version even if it is ugly — a named deputy, an expiry, a recorded admin override. Shipping a gate with no away path is how you train people to walk around it.
Closing thought
When someone asks you to add an approval, they are asking you to encode a piece of how the organisation accepts risk. If you only add a boolean, you have encoded a screenshot. The domain will still happen. It will happen in the gaps.