Overview
An HR platform for Indian SMBs must solve a uniquely complex problem: compute accurate payroll for a diverse workforce (full-time, part-time, contractual) while staying compliant with PF, ESI, TDS, PT, LWF, and Gratuity regulations - all of which have different computation rules, thresholds, and filing deadlines. This guide covers the architectural decisions behind IdeaSprout's HR module.
1. Employee Data Model
The Core Challenge
Employee data is not static. Salary changes, promotions, department transfers, and tax declarations all affect payroll computation. The system must know the exact state of an employee's record on any given date - not just the current state.
Temporal Data Pattern
We use a bi-temporal data model for all payroll-affecting employee attributes:
CREATE TABLE employee_salary_history (
id UUID PRIMARY KEY,
employee_id UUID NOT NULL,
basic_salary NUMERIC(12,2),
hra NUMERIC(12,2),
special_allowance NUMERIC(12,2),
effective_from DATE NOT NULL,
effective_to DATE, -- NULL means currently active
created_at TIMESTAMPTZ DEFAULT NOW()
);To compute payroll for March 2026, the engine queries salary records where effective_from <= 2026-03-31 AND (effective_to IS NULL OR effective_to >= 2026-03-01). Mid-month salary changes are prorated automatically.
2. Payroll Computation Engine
Computation Order (Critical)
Payroll components have dependencies - computing them out of order produces wrong results:
1. Gross Salary = Basic + HRA + Allowances
2. Attendance Deduction = (Gross / Working Days) × Absent Days
3. Adjusted Gross = Gross - Attendance Deduction
4. PF Employee = min(Adjusted Basic, 15000) × 12%
5. ESI Employee = Adjusted Gross × 0.75% [if Gross ≤ 21,000]
6. Professional Tax = lookup(state, Adjusted Gross)
7. TDS = compute_tds(annual_projected_income, declarations)
8. Net Pay = Adjusted Gross - PF Employee - ESI Employee - PT - TDS - Other DeductionsTDS Computation Complexity
TDS is the most complex component because it requires annual income projection:
Annual Projected Income =
(YTD Gross) + (Remaining Months × Current Month Gross)
- Standard Deduction (₹50,000)
- HRA Exemption (min of: actual HRA, 50%/40% of basic, rent paid - 10% of basic)
- Section 80C declarations (max ₹1,50,000)
- Section 80D declarations (max ₹25,000)
- NPS (Section 80CCD) declarations
TDS Per Month = (Tax on Annual Projected Income - TDS Already Deducted) / Remaining MonthsThis means TDS changes every month as the projection updates. The engine recomputes the full annual projection on every payroll run.
Idempotent Payroll Runs
Payroll runs are idempotent - running the same month twice produces the same result and does not create duplicate records. Each run is identified by (employee_id, payroll_period). A re-run overwrites the draft; once a run is "locked" (payslips distributed), re-runs create a revision record instead.
3. Leave Accrual Engine
Accrual Policies
Different leave types accrue differently:
| Leave Type | Accrual Rule | Max Balance | Carry Forward |
|---|---|---|---|
| Earned Leave (EL) | 1 day per 20 days worked | 45 days | Yes (max 30) |
| Casual Leave (CL) | 1 day per month (credited upfront) | 12 days | No |
| Sick Leave (SL) | 0.5 days per month | 12 days | No |
| Maternity Leave | 26 weeks (one-time) | N/A | N/A |
Accrual Computation
The accrual engine runs on the last day of each month:
def accrue_earned_leave(employee, month):
days_worked = attendance_service.get_days_worked(employee, month)
# Days worked includes: present days + paid leaves
# Excludes: unpaid leaves, LOP days
accrual = floor(days_worked / 20)
current_balance = leave_balance_service.get(employee, 'EL')
new_balance = min(current_balance + accrual, MAX_EL_BALANCE)
leave_balance_service.update(employee, 'EL', new_balance)Leave Balance Consistency
Leave balances are maintained as a running ledger (not a single balance field):
CREATE TABLE leave_transactions (
id UUID PRIMARY KEY,
employee_id UUID,
leave_type TEXT,
txn_type ENUM('accrual', 'deduction', 'encashment', 'lapse', 'opening'),
days NUMERIC(4,1),
reference_id UUID, -- leave_request_id or payroll_run_id
txn_date DATE,
balance_after NUMERIC(5,1)
);Current balance = SUM(days) WHERE employee_id = X AND leave_type = Y. The balance_after column is a denormalised snapshot for fast reads.
4. Statutory Compliance Pipeline
Filing Calendar
| Compliance | Frequency | Deadline | Penalty for Late Filing |
|---|---|---|---|
| PF ECR (EPFO) | Monthly | 15th of next month | ₹5,000 + 12% p.a. interest |
| ESI Return | Monthly | 15th of next month | ₹5,000 |
| TDS Return (Form 24Q) | Quarterly | 31st of month after quarter | ₹200/day |
| Professional Tax | Monthly/Quarterly | Varies by state | Varies |
| Form 16 (TDS Certificate) | Annual | 15th June | ₹100/day |
Automated Challan Generation
The compliance pipeline generates statutory challans automatically after payroll lock:
- 1PF ECR: Aggregate
pf_employee + pf_employerby UAN, generate ECR file in EPFO-specified format - 2ESI: Aggregate ESI contributions by IP number, generate return in ESIC portal format
- 3TDS: Aggregate TDS by PAN, generate Form 24Q in NSDL-specified format
All generated files are stored with a hash for integrity verification and linked to the payroll run.
5. Audit Trail Design
What Must Be Auditable
Indian labour law requires employers to maintain payroll records for 8 years. The audit trail must capture:
- •Every payroll computation with all inputs (salary, attendance, declarations)
- •Every leave transaction with approver and timestamp
- •Every statutory filing with submission proof
- •Every salary revision with approver
Implementation
All payroll-affecting changes go through an event sourcing pattern. The current state is a projection of all events. Events are immutable - corrections create new events, not overwrites.
CREATE TABLE hr_events (
id UUID PRIMARY KEY,
event_type TEXT, -- 'salary_revised', 'leave_approved', 'payroll_locked'
entity_id UUID,
payload JSONB,
actor_id UUID,
occurred_at TIMESTAMPTZ DEFAULT NOW()
);6. Common Architectural Pitfalls
| Pitfall | Consequence | Fix |
|---|---|---|
| Mutable salary records | Cannot recompute historical payroll correctly | Bi-temporal salary history |
| TDS computed on current month only | Wrong TDS, employee gets surprise tax bill in March | Annual projection recomputed monthly |
| Leave balance as single field | Race conditions on concurrent approvals | Ledger-based leave transactions |
| Payroll not idempotent | Duplicate payslips on re-run | Upsert on (employee_id, period) |
| Compliance files generated manually | Filing delays, human error | Automated challan generation post-lock |
Summary
A production HR platform for Indian SMBs requires bi-temporal employee data, a dependency-ordered payroll engine with annual TDS projection, a ledger-based leave accrual system, and an automated compliance pipeline that generates statutory challans without manual intervention. The audit trail must be append-only and retained for 8 years.