Engineering governance · Head of Development · Feb 2026

One approval module, two designs: a rebuild and a standard

A pair spent two weeks on an approval design that forced one account per role — caught in review before launch, rebuilt in a week with a pattern three other projects already used. The team never knew it existed, and that gap became a 40-standard analyst handbook.

1 wk
Rebuild after the design review
40
Standards in the analyst handbook
Design reviewRBACSOPLaravel

Executive Summary

Three months before launch, an analyst–developer pair delivered an approval module for the learning-management SaaS — two weeks of work on a design that made every approver carry a second account, where a headmaster-teacher had to log out and back in to approve their own submission. The review caught it before launch. A week rebuilt the module on the pattern three or four other projects already ran; the pair had simply never worked on any of them. That gap — not the design — became the standard: a numbered analyst handbook, 40 entries and counting.


The Problem & Context

  • The project: a school/learning-management SaaS three months from launch, finalizing the approval module among the last few before release.
  • The team: one analyst and one backend developer — both with solid history on other modules of the same SaaS, and no exposure to how approval problems had already been solved elsewhere.
  • The process: I run several projects at once and lean on the review process of the analyst assigned to each. Two weeks in, the pair reported the module done in alpha and testing. I reviewed it and found a design that worked only on paper.

Two Designs

My idea of the module was configuration, not workflow:

  • One approval_types table — transaction approvals, report approvals, month-closing approvals. A new kind of approval is a row, not a code path.
  • A many-to-many to users, split by intent: who can submit each type, who can approve it.
  • The approval list then shows each user exactly what they may act on: pick from your queue, approve. Done.

The pair’s design put the choice at creation time: whoever submits picks the person who must approve it. Not entirely wrong — but every submission now carried permission logic, and it leaned on a hard platform constraint: at the time, one user had one role.

That constraint produced the nightmare:

sequenceDiagram
    participant H as Headmaster (teacher account)
    participant S as LMS
    H->>S: Submits an approval
    Note over H,S: In this design a submitter cannot approve, and an approver cannot submit
    H->>S: Logs out
    H->>S: Logs back in (headmaster account)
    S-->>H: The same submission waits in this account's queue
    H->>S: Approves the request they filed themselves
    Note over H: One person, two accounts, because role and account were the same thing

The origin was real: one school required that a user cannot approve their own submission — a reasonable maker–checker rule. But the pair implemented it as a law of the product, and let it reshape the whole workflow around it.


The Rewrite

I stopped the build and pivoted a second analyst — someone who had worked on the other projects — to transfer knowledge, not code: the approach, a working example, and the traps. The original pair implemented the rewrite themselves.

One week. Same pair. The schema settled into this shape:

erDiagram
    USERS }o--o{ ROLES : "holds"
    USERS ||--o{ USER_APPROVAL_ACCESS : "is granted"
    APPROVAL_TYPES ||--o{ USER_APPROVAL_ACCESS : "grants on"
    USERS ||--o{ APPROVALS : "submits"
    APPROVAL_TYPES ||--o{ APPROVALS : "classifies"

    APPROVAL_TYPES {
        int id
        string name
    }
    USER_APPROVAL_ACCESS {
        int user_id
        int approval_type_id
        bool can_submit
        bool can_approve
    }
    APPROVALS {
        int id
        int approval_type_id
        int submitted_by
        int approved_by
        string state
    }

The school’s rule survived — as a filter, not a workflow:

Simplified, sanitized mock of the approval-list scope. Laravel, not production code.

// The approval list: the types this user may approve, minus their own
// submissions. The maker-checker rule stays a filter on the list, so
// one account can submit and approve across roles - no logout dance.
public function scopeApprovableBy(Builder $query, User $user): Builder
{
return $query
->whereHas('type.accessUsers', fn (Builder $q) => $q
->where('user_id', $user->id)
->where('can_approve', true))
->where('submitted_by', '!=', $user->id);
}

The Standard

I run multiple projects at the same time, so I lean on the review process of the analyst assigned to each. That trust is what failed here — not the pair’s effort and not their skill. A review system has to stand on several fronts and several people before one analyst’s judgment can carry it alone. So the lead analyst and I wrote the missing piece down.

The result is a numbered analyst handbook — 40 standards, grouped in three sections, last updated July 2026, with an e-commerce annex so far limited to cart and checkout:

Section Range Count
Features — auth, access rights, master data and CRUD, transactions, uploads, exports and reports, notifications, audit trail STD-BAS-001 – 024 24
Design and flow — lists and tables, forms and inputs, messages and feedback, terminology and format STD-BAS-025 – 037 13
Documentation — requirement writing STD-BAS-038 – 040 3

Three rules hold it together: analysts document standardized modules from every project in one source; a new problem goes through a peer consult first — analyst to analyst, developer to developer — to check whether it has already been solved; and documents pass analyst-lead plus peer review before any handoff to development.

The handbook was overdue in a second way. Months later, the recurring gaps on other projects were exactly the class of thing it is written to catch before handoff: a login form with no forget-password flow; a manual-transfer flow with no admin step to accept and verify the payment; a history page that broke when its item had since been archived.


Results & Impact

The rewrite shipped, and so did the suite around it — accounting and the rest of the modules launched on schedule. The approval module has been in production since.

The single-role constraint is gone: one user, many roles. I asked for it, the pair built it, and the confirmation came through the project lead after a May 2026 reorganization — delegation working as intended, and a reminder to see the important things with my own eyes when I can.

The handbook is now a maintained artefact: 40 numbered standards, updated July 2026, with the e-commerce annex for cart and checkout.


What I’d do differently

  • Review at the first hour, not at “done”. Two weeks of solid work went into a design that should not have survived day one of review. The handbook’s review layer now makes a design checkpoint explicit before development starts.
  • Client-specific rules live in data. One school’s maker–checker rule became everyone’s workflow. The rewritten version keeps the same rule as a filter on the list. Any rule that can differ per client is a setting, never a code path.
  • Knowledge travels with exposure, not reminders. The pair didn’t lack skill — they lacked exposure. Crossing people between projects earlier is the real fix; the handbook is the safety net for the times they haven’t crossed yet.
  • Trusting a process is not the same as checking it. Review now comes from several people — analyst lead and peer — before a design reaches development.

Next case file

The foodcourt deal we lost: conflict rules for a multi-device POS

A second foodcourt put several waiters and one cashier on shared orders — and the offline-first POS had no answer for two devices disagreeing. Kafka broadcasts shipped in a week; the conflicts they exposed took six, and we lost the client before the fix shipped. The rules went to production anyway.

KafkaOffline-firstConflict resolutionDistributed systemsTeam leadership
Mar 2025
Head of Development
6 wksFrom a 2-week estimate to shipped conflict rules