Calculating salaries looks like arithmetic: hours times rate, subtract tax, done. Until you actually build it. This article distills the most important architectural decisions I made building the HRIS & Payroll module in Dexova — and the mistakes I was lucky to catch early.
Why payroll must not be computed from live data
The first mistake almost every amateur payroll system makes: computing salaries directly from an attendance table that can still change. An employee files a correction to their clock-in, HR approves it — and a payslip that was already paid suddenly no longer matches the data.
The fix is a locked period. In Dexova, the monthly attendance recap locks the attendance data as a snapshot; a payroll run can only be generated from a period that is already locked. Corrections after the lock flow into the next period as adjustments — not by rewriting history.
Regulatory rules as configuration, not if-else
Overtime in Indonesia is tiered (PP 35/2021): the first hour and subsequent hours have different multipliers, and holidays are different again. Writing that as a chain of if-else means every regulatory or company-policy change is a new deploy.
In Dexova, overtime rates, the daily-rate divisor, rounding strategy, and work-week type are per-company configuration. The code only knows how to evaluate a rule; the rule's values belong to data. A nice side effect: testing a calculation becomes a matter of preparing a config + attendance input, then comparing the output.
- Tiered overtime rates for the 1st hour / subsequent hours / holidays — configuration.
- Cutoff and payday are separated; payday shifts automatically on bank holidays.
- Salary components and per-employee rates (pro-rata for new hires) — data, not code.
- One-off adjustments (bonuses, THR, severance) live on the run, not as permanent components.
The payroll run as an async job
Computing salaries for hundreds of employees is not a single-HTTP-request job. A payroll run in Dexova executes as an async job with progress monitoring: HR presses one button, watches the progress, and gets a result they can review before finalizing.
The same pattern powers bulk employee import from Excel — any heavy work touching many rows must not block the UI, and must be trackable.
Attendance: guard validity at the source
Payroll is only as good as its attendance data. That is why validation happens at the point of entry: check-in via a PWA with geofencing (the app detects the nearest office and rejects clock-ins outside the radius), WFH via an approved two-address model, and attendance corrections that always go through approval — not free edits in the database.
Cleaning up dirty data later is always more expensive than rejecting it up front.
Summary
If it has to fit in one sentence: separate the rules from the code, lock the data before computing, and make heavy work a trackable job. Those three are what make payroll auditable — and what makes sleeping easier on payday.