Building a Payroll System That Didn't Make Anyone Cry
Payroll is the one module where bugs cost real money. Here's how we designed permissions, audit trails, and a calculation engine that survived 8 companies.
Why Payroll Is Hard
Payroll is the one ERP module where a bug costs someone real money, on a specific day, in a way that's immediately visible. Overpay an employee and clawing the difference back is administratively painful and damages trust. Underpay someone and you've created a retention and morale problem that no apology fully undoes. Get a tax calculation wrong at scale and the government notices, turning an engineering bug into a compliance issue with real deadlines and legal exposure. The garment industry adds layers: overtime rules that vary by shift and role, shift differentials that change on holidays, piece-rate pay reconciled against production output rather than hours, attendance bonuses with seasonal rules, loan deductions tracked against balances over multiple cycles.
Each of the eight factories had its own variation on these rules, so the calculation engine had to be configurable at the tenant level with more than 40 configurable rules per tenant. That configurability was the only way to ship a single system to eight factories, but it meant the engine was effectively a small domain-specific language for payroll computation. Getting it right required testing against real historical payroll data from each factory before go-live. We ran parallel payroll runs (new system alongside old) for two full pay cycles at each factory before cutover, and the discrepancies those parallel runs surfaced were the most valuable testing we did.
Permission System Design
The permission model had to be granular enough that an HR manager could edit an employee's salary structure without viewing the payroll amounts of every employee. We implemented role-based permissions with resource-level actions — view, create, edit, approve — each independently granted. The "approve" action was separate from "edit" because the two represent different responsibilities: editing is operational work, approval is a control exercised by someone with authority. Mixing them would mean either letting anyone who can edit approve anything (no control) or restricting edits to approvers only (operational bottleneck).
The approval workflow was the hardest part. A salary change requires manager approval, then HR head approval, then finance verification before it takes effect. Each step logs who took the action, what they changed, and when. The chain had to support rejection at any step with a required reason, and rejection sent the change back to the previous step rather than restarting the whole flow, because restarting would lose the work done at earlier steps. That required careful state machine modeling, and we went through three iterations before settling on the version that handled all the edge cases.
Audit Trail
Every payroll run generates a complete audit trail: who initiated it, what parameters they used, which employees were included, what was calculated for each line item, and whether anyone manually overrode a value. The audit log is append-only — entries can be added but never deleted or modified — which guarantees no actor, including administrators, can rewrite history without leaving evidence. That guarantee is what makes the audit trail trustworthy rather than just a log someone could quietly edit.
Manual overrides were the most important thing to capture, because they're where human judgment enters the calculation. The system might calculate overtime as X, but a clerk who knows the employee was sent home early might override to Y. That override is legitimate but also where errors or fraud could enter, so capturing who made it, what changed, and why (via a required reason field) was non-negotiable. The reason field is free-text rather than a dropdown because the set of legitimate override reasons is open-ended, and a dropdown would force users to pick the closest match rather than describe what actually happened. That level of transparency — the ability to trace any number on a payslip back to the exact rule and input data — was the feature that sold the module internally, because trust in an automated system isn't declared; it's earned by being verifiable.
