The problem
At CloudJet I worked on the backend of an HRM and KPI evaluation platform. The product covers employee profiles, departments, KPI cycles, goal setting, performance reviews and approvals.
A performance review touches several people. An employee sets goals and submits results, a department manager reviews them, HR staff follow the cycle, and administrators manage the setup. Each of these four roles should see a different slice of the data. An employee should not read a colleague’s review, and a manager’s approval queue should only hold their own department’s submissions.
Reviews also feed decisions about people, so the history matters. Who approved what, who rejected it and when has to be traceable later. And the reporting side has its own load problem: exporting Excel or PDF reports and loading dashboards can get slow if every request does all the work inline.
What I did
Normalized schemas around the review lifecycle
I designed and implemented the Django REST backend and its PostgreSQL schemas for employees, roles, KPI templates, evaluation criteria, goals, review results, approval history and audit logs. KPI templates and evaluation criteria live in their own tables, apart from the goals and review results recorded against them.
The trade-off is more tables and more joins than a flat design. I accepted that because a normalized model keeps one source of truth for each fact, which matters when scores and approvals are questioned later.
Role-based access for four roles
I implemented role-based access control for administrators, HR staff, department managers and employees. Access rules limit which records, which KPI data and which approval queues each role can reach, and the API enforces them on the server side.
The cost is that every new endpoint needs its access rules thought through and tested. That slows feature work a little, and it is the right place for the slowdown, because a missed rule on review data is a privacy problem.
Weighted scoring with audit-friendly status tracking
I developed weighted KPI scoring and the individual and department summaries built on it. Each submission moves through tracked statuses, and approval history and audit logs record the path it took.
Keeping a full trail means storing more rows and writing more code on every status change. In return, a disputed score can be explained from the data instead of from memory.
Notifications tied to workflow events
I built notification workflows for submissions, approvals, rejections and reminders, so people learn that something needs their action without checking the app.
The trade-off is extra moving parts around every status change, and one more thing to test for each step of the flow. I accepted it because a review that waits unnoticed in someone’s queue stalls the whole cycle.
Heavy reports in the background, lighter dashboard APIs
I built asynchronous Excel and PDF report generation using AWS SQS and ECS. The API puts a report job on a queue, and a worker produces the file outside the request. For dashboards, I optimized the APIs with filtering, pagination and selective prefetching, and documented them with OpenAPI and covered them with tests.
Background reports mean the user waits for a file instead of getting it in the same response, and there is one more service to deploy and monitor. That was better than tying up web requests on long exports.
Result
The platform has one review flow from goal setting through submission, review and approval, with access scoped to four roles. Approval history and audit logs make that flow traceable. Report generation runs in the background through a queue and workers, and the dashboard APIs are documented and tested. I have no measured performance or usage numbers to report from this project, so I keep the outcome qualitative.
What I’d do differently
I would write the access rules down as a single permission matrix, per role and per resource, at the start, and generate tests from it. That would make it easier to review the rules and harder to miss one when a new endpoint arrives. I would also add timing measurements to the report workers and dashboard APIs from day one, so I could show the effect of the optimizations with numbers.