Step 11 of 11
4-service system: Clean Architecture, RabbitMQ, Thai tax, PDF payslips, full-stack integration
Final Workshop: ระบบ Payroll & Benefits ครบทุกส่วน — Clean Architecture, RabbitMQ, PDF, Docker
Roadmap: dotnet-csharp-backend Challenge Repo: https://github.com/TP-Coder-Innovation-Hub/payroll-and-benefits-management-challenge Architecture: 4 microservices | ASP.NET Core 9 | Clean Architecture
Build a payroll and benefits management platform for mid-size Thai companies. The system handles employee records, monthly salary calculation with Thai progressive tax, social security deductions, provident fund contributions, benefit enrollment, leave tracking, and payslip/report generation.
HR admins use a Vue 3 dashboard. Employees access self-service features (payslips, leave balance, benefits) via an Android Jetpack Compose app.
Loading diagram...
Loading diagram...
Responsibility: Employee CRUD, departments, salary grades.
| Feature | Acceptance Criteria |
|---|---|
| Create/update employee | POST/PUT returns employee with generated ID; validates Thai national ID format |
| List employees | GET with pagination, filtering by department/salary grade |
| Manage departments | CRUD for departments; each employee belongs to one department |
| Salary grades | Define grade bands (min/max salary); validation on assignment |
| Employee search | Search by name, employee code, department |
Database: SQL Server — tables: Employees, Departments, SalaryGrades
Responsibility: Salary calculation, Thai tax, deductions, payroll runs.
| Feature | Acceptance Criteria |
|---|---|
| Calculate monthly payroll | Computes gross, deductions, net from employee + salary grade |
| Thai progressive tax | Applies 2024 brackets: 0-150k (0%), 150k-300k (5%), 300k-500k (10%), 500k-750k (15%), 750k-1M (20%), 1M-2M (25%), 2M-5M (30%), 5M+ (35%) |
| Social security deduction | 5% of salary, capped at 750 THB/month (employer + employee each) |
| Provident fund | Employee contribution 2-15%, employer match configurable per policy |
| Scheduled payroll run | IHostedService triggers monthly run on configurable cron; publishes PayrollCalculated event per employee to RabbitMQ |
| Payroll history | Store monthly payroll records per employee; queryable by period |
Database: SQL Server — tables: PayrollRecords, TaxBrackets, DeductionPolicies, PayrollRuns
Events published to RabbitMQ:
payroll.calculated — per employee, payload: employeeId, period, gross, deductions, netpayroll.run.completed — per payroll run, payload: runId, period, totalEmployees, totalAmountResponsibility: Benefit enrollment, leave tracking, balances.
| Feature | Acceptance Criteria |
|---|---|
| Enroll in benefits | Employee enrolls in health insurance plan and/or retirement fund; validates eligibility |
| Health insurance plans | CRUD for plans with coverage levels, premium rates |
| Leave request | Submit leave request (sick, personal, vacation); validates against balance |
| Leave balance | Track entitlement, used, remaining per leave type per year |
| Leave approval workflow | Manager approves/rejects; status transitions enforced |
Database: SQL Server — tables: BenefitPlans, Enrollments, LeaveTypes, LeaveBalances, LeaveRequests
Responsibility: Payslip PDF generation, payroll reports, tax reports.
| Feature | Acceptance Criteria |
|---|---|
| Generate payslip PDF | Consumes payroll.calculated event, generates PDF with salary breakdown, deductions, net pay; stores and exposes download endpoint |
| Monthly payroll summary report | Aggregated report per department: total gross, total deductions, total net |
| Annual tax report (PND1 style) | Yearly employee tax summary grouped by income bracket |
| Report history | List and download previously generated reports |
Database: SQL Server — tables: Payslips, Reports
Events consumed from RabbitMQ:
payroll.calculated — trigger payslip generationpayroll.run.completed — trigger summary report generationAdmin, HR, Manager, Employee[Authorize(Roles = "Admin,HR")] — employee management, payroll runs[Authorize(Roles = "Admin,HR,Manager")] — leave approval[Authorize] — employee self-service (own data only)| Pattern | Usage | Protocol |
|---|---|---|
| Synchronous | employee-service → payroll-service (salary data), employee-service → benefits-service (eligibility) | REST (HttpClient) |
| Asynchronous | payroll-service → reporting-service (payslip generation) | RabbitMQ (pub/sub) |
Taxable Income Rate
0 - 150,000 0%
150,001 - 300,000 5%
300,001 - 500,000 10%
500,001 - 750,000 15%
750,001 - 1,000,000 20%
1,000,001 - 2,000,000 25%
2,000,001 - 5,000,000 30%
5,000,001+ 35%
| Constraint | Requirement |
|---|---|
| Framework | ASP.NET Core 9 (LTS) |
| Architecture | Clean Architecture per service (Domain, Application, Infrastructure, Presentation) |
| ORM | Entity Framework Core with code-first migrations |
| Database | SQL Server (containerized) |
| Messaging | RabbitMQ via MassTransit or raw RabbitMQ client |
| Auth | JWT Bearer tokens, role-based policies |
| QuestPDF, iTextSharp, or equivalent for payslip generation | |
| Containerization | Docker Compose for all services + dependencies |
| C# features | Records for DTOs, pattern matching, file-scoped namespaces |
| CI | Pipeline in challenge repo (build, test, lint on PR) |
Context: Each microservice needs clear separation between business logic and infrastructure.
Decision: Apply Clean Architecture with 4 layers per service: Domain (entities, enums), Application (use cases, interfaces), Infrastructure (EF Core, RabbitMQ, external APIs), Presentation (controllers, middleware).
Consequence: More files and projects per service, but testable business logic independent of frameworks. Dependency rule: outer layers depend on inner, never inward-out.
Context: Payroll calculation and payslip/report generation have different throughput and reliability needs.
Decision: Publish payroll.calculated and payroll.run.completed events to RabbitMQ. Reporting-service consumes asynchronously.
Consequence: Decouples payroll processing from report generation. Requires idempotent consumers and dead-letter handling. Adds RabbitMQ as infrastructure dependency.
Context: Each service owns its data. No shared database.
Decision: Each microservice has its own SQL Server database (containerized separately or same server with separate schemas).
Consequence: Data consistency across services requires eventual consistency via events or REST calls. No cross-service JOINs. Simpler schema evolution per service.
Context: Payroll must run on a schedule (monthly, configurable date).
Decision: Use IHostedService with BackgroundService base class and a timer/cron expression. Trigger payroll calculation for all active employees.
Consequence: Simple in-process scheduling. For production, consider moving to Hangfire or Quartz for persistence and retry. Current approach suffices for workshop scope.
Context: payroll-service and benefits-service need employee data from employee-service.
Decision: Use REST calls (via IHttpClientFactory). Each service exposes internal endpoints for service-to-service calls.
Consequence: Synchronous coupling — employee-service downtime blocks payroll. Acceptable for workshop scope. In production, consider caching or a service mesh.
src/
EmployeeService/
EmployeeService.Domain/ # Entities, Value Objects, Enums
EmployeeService.Application/ # Use Cases, DTOs (records), Interfaces
EmployeeService.Infrastructure/ # EF Core DbContext, Repositories, Migrations
EmployeeService.Presentation/ # Controllers, Middleware, Program.cs
EmployeeService.Tests/ # Unit + Integration tests
PayrollService/ # Same structure
BenefitsService/ # Same structure
ReportingService/ # Same structure
services:
employee-api:
payroll-api:
benefits-api:
reporting-api:
sqlserver:
rabbitmq:
employee-db:
payroll-db:
benefits-db:
reporting-db:
docker compose upPrevious
Workshop 4: Payslip PDF Service
Final step