Case study · Government Payroll
ePayroll
National payroll platform covering employee validation, payroll data, payslip generation and reporting for the public sector.
- Payslip generation turnaround
- ~7 days → ~24 hours
- Role
- Backend Engineering · System Design · Technical Leadership
- Team
- Co-led ~4 engineers
- Domain
- Government Payroll
Overview
ePayroll supports auxiliary payroll activities for government employees: validating that employees are in the right state, maintaining payroll entries, generating payslips and producing reports. It is split into three applications — PayAdmin for master records and payroll data, PayVerify for validation by management units, and PayServices for employee-facing services such as payslips.
The problem
The previous process took roughly a week to produce payslips after payroll data arrived, and reports only became available hours after validation had finished. Validation, administration and employee services needed to share data reliably while operating on more than twenty million records.
My role
- Built and maintained APIs for PayAdmin, PayVerify and PayServices.
- Designed and implemented the payslip generation service.
- Wrote the synchronization service moving validated data from PayVerify into PayAdmin.
- Wrote the report queries behind operational and management reporting.
- Built the service that keeps user identities synchronized with Keycloak.
- Advised the frontend team on API contracts and implementation.
- Co-led a team of around four engineers and mentored junior developers.
Architecture
-
Applications
- Administration portal Master records and payroll entries
- Validation portal Management-unit validation
- Employee services Payslips and self-service
-
Services
- Domain APIs One per application
- Payslip generation Background service
- Validation sync Verify → Admin
- Identity sync Users → Keycloak
-
Platform
- PostgreSQL 20M+ records
- Keycloak Identity & access
- Azure AKS Containerised workloads
Scale
- Records
- 20M+
- Employee, payroll and validation history on PostgreSQL.
- Applications
- 3
- Administration, validation and employee services sharing one domain.
- Engineers co-led
- ~4
Engineering challenges
Working with 20M+ records
Payroll history, validation results and employee master data grow every cycle. Queries that are harmless on a development dataset become the slowest part of the system in production, and reporting queries compete with transactional work for the same tables.
Generating payslips at volume
Payslips have to be produced for every employee in a cycle, reflect validated data only, and be re-generated safely if a run is interrupted. Doing this inside a request/response API is not practical at this scale.
Keeping three applications consistent
Validation happens in one application, administration in another, and employees consume the results in a third. Data has to move between them without being lost, duplicated or applied out of order.
Identity synchronization
Access is controlled through Keycloak, but the people who need access are defined by payroll and organizational data. The two have to stay aligned as staff join, move or leave.
Approach
- Background processing for heavy work. Payslip generation runs as a dedicated background service that processes work in batches, so a run can be monitored, resumed and retried without blocking user-facing APIs.
- The right data-access tool for each path. Entity Framework handles most transactional workflows; Dapper and hand-tuned SQL handle bulk reads and reporting, where control over the query shape matters.
- Reporting designed around query plans. Report queries were written and indexed against production-sized data so they can be run on demand instead of being pre-computed hours later.
- Explicit synchronization services. Moving validated data and identity changes are separate, idempotent services with clear ownership, which makes them safe to re-run.
- Containerised deployment. Services run on Azure Kubernetes Service, giving the team consistent environments and independent scaling for background workloads.
Impact
~7 days → ~24 hours
Payslip generation
Generation runs as a background process instead of a manual, sequential job.
Hours → on demand
Report availability
Reports are generated when required rather than hours after validation completes.
Technologies
- .NET
- C#
- PostgreSQL
- Entity Framework
- Dapper
- Azure AKS
- Kubernetes
- Docker
- Keycloak
Lessons
- 01 Batch work on large tables is mostly a data-access problem. Choosing where to use EF Core and where to drop to Dapper and hand-written SQL mattered more than any application-level optimization.
- 02 Synchronization between applications needs explicit ownership of each record and a way to re-run safely. Treating sync as a first-class service made failures recoverable instead of manual.
- 03 Identity data drifts. Keeping Keycloak in step with the system of record is easier as a continuous process than as a one-off migration.