Multi-Tenant Payroll System README
A comprehensive, production-ready payroll management platform built with Spring Boot 3.5.8 and Java 21, designed for multi-tenant SaaS deployments with AI-powered insights and compliance automation.
The Multi-Tenant AI Payroll System is an enterprise-grade solution for managing payroll operations across multiple organizations. It implements shared-schema multi-tenancy, automated payroll calculations, role-based access control, and Google Generative AI integration for intelligent payroll analytics and insights.
Key Highlights:
- Tech Stack
- Architecture
- Database Schema
- Getting Started
- Configuration
- API Endpoints
- Multi-Tenancy
- Security
- AI Features
- Development Workflow
- Testing
- Deployment
- Troubleshooting
| Component | Technology | Version |
|---|---|---|
| Java | OpenJDK | 21+ |
| Framework | Spring Boot | 3.5.8 |
| Build Tool | Maven | 3.9+ |
Backend:
spring-boot-starter-webβ REST API frameworkspring-boot-starter-data-jpaβ ORM with Hibernatespring-boot-starter-validationβ Bean validationspring-boot-starter-oauth2-resource-serverβ OAuth2 authenticationspring-boot-starter-actuatorβ Health checks & metricsspring-boot-starter-data-redisβ Caching layer & JWT Session managerspring-ai-starter-model-google-genaiβ Google Generative AI integrationlombokβ Reduce boilerplate code
Database:
org.postgresql:postgresqlβ PostgreSQL driverspring-boot-docker-composeβ Auto-start PostgreSQL + Redis via Docker
Observability:
micrometer-registry-prometheusβ Prometheus metrics export
Testing:
spring-boot-starter-testβ JUnit 5, Mockitotestcontainersβ PostgreSQL & Redis containers for integration testsspring-boot-testcontainersβ Seamless TestContainers integration
Controller Layer (REST endpoints) β Service Layer (Business logic, transactions, AI calls) β Repository Layer (Database queries, JPA) β Entity Layer (JPA models with @Entity, @Table) β Database (PostgreSQL with tenant_id on all tables)
Shared Schema, Separate Data:
- Single PostgreSQL database shared by all tenants
- Every table has
tenant_idcolumn (non-nullable, indexed) - Row-level security: all queries filter by
tenant_id - No cross-tenant data leakage by design
All entities extend BaseModel which provides common fields:
id: Unique identifier (UUID)createdAt: Timestamp of creationupdatedAt: Timestamp of last updatecreatedBy: User who created the recordupdatedBy: User who last updated the record
All API responses follow the Result<T> pattern:
public record Result<T>(
boolean flag,
String message,
T data
) {
public static <T> ResponseEntity<Result<T>> success(String message, T data) {
return new ResponseEntity<>(new Result<>(true, message, data), HttpStatus.OK);
}
// All other implementations...
}@Entity
@Table(name = "tenants")
public class Tenant extends BaseModel {
@Column(nullable = false, unique = true)
private String name;
@Column(unique = true)
private String subdomain;
@Column(name = "is_active")
private boolean active = true;
// Getters, setters, and additional fields
}- All entities must extend
BaseModelfor auditing - Use
@CreatedByand@LastModifiedByfor user tracking - Foreign keys should include
tenant_idfor multi-tenancy
Tenant Management:
tenantsβ Organization profiles (company name, email, status)usersβ User accounts with roles (admin, hr_officer, payroll_officer, employee)
Organization Structure:
departmentsβ Business units (marketing, engineering, etc.)positionsβ Job roles (senior engineer, junior manager)employeesβ Employee master records with employment details
Salary Configuration:
salary_structuresβ Base salary templatesemployee_salary_assignmentsβ Employee-to-salary mappings (salary history)allowancesβ Bonuses, HRA, shift allowance, etc.deductionsβ Tax, insurance, loan recovery, etc.
Payroll Processing:
payroll_runsβ Monthly/bi-weekly payroll batchespayroll_detailsβ Per-employee calculated payrollpayroll_detail_allowancesβ Join table: allowance amounts per payrollpayroll_detail_deductionsβ Join table: deduction amounts per payroll
Time Tracking:
attendanceβ Daily check-in/check-out recordsovertimeβ Overtime hours with multipliers
Payments & Tax:
tax_calculationsβ Income tax, professional tax per payrollpayment_methodsβ Bank transfer, cash, chequeemployee_payment_methodsβ Employee bank account detailspaymentsβ Salary transfer records with status
See ERD (Eraser.io format) for visual schema.
- Java 21+ β Download
- Maven 3.9+ β Download
- Docker & Docker Compose β Download
- Git β Version control
git clone https://github.com/DroidZeroCodes/multi-tenant-ai-payroll-system.git cd multi-tenant-ai-payroll-system
The project includes docker-compose.yml for PostgreSQL and Redis auto-startup:
./mvnw spring-boot:run
This automatically starts:
- PostgreSQL on
localhost:5432(database:payroll-system-db) - Redis on
localhost:6379(caching layer)
Or manually:
docker-compose up -d
./mvnw clean install
./mvnw spring-boot:run
Or via JAR:
java -jar target/multi-tenant-ai-payroll-system-0.0.1-SNAPSHOT.jar
curl http://localhost:8080/actuator/health
Expected response:
{ "status": "UP", "components": { "db": { "status": "UP" }, "redis": { "status": "UP" } } }
Create .env file or set in application.yml
- Development Environment:
application-dev.yml - Production Environment:
application-prod.yml
Define the following environmental variables:
- SPRING_DATASOURCE_USERNAME
- SPRING_DATASOURCE_PASSWORD
- SPRING_DATASOURCE_DB
Define the following environmental variables:
- ${GOOGLE_GENAI_API_KEY}
The following endpoints have been exposed (add more as you see fit):
- health
- metrics
- prometheus
{
"success": true,
"message": "Operation completed successfully",
"data": {
},
"errors": null
}{
"success": false,
"message": "Failed to process request",
"data": null,
"errors": [
{
"status": "BAD_REQUEST",
"code": "VALIDATION_ERROR",
"title": "Validation Error",
"detail": "Name must not be blank",
"source": {
"pointer": "/name"
}
}
]
}200 OK: Successful GET, PUT, or PATCH requests201 Created: Resource successfully created204 No Content: Successful DELETE requests400 Bad Request: Invalid request format or validation errors401 Unauthorized: Authentication required403 Forbidden: Insufficient permissions404 Not Found: Resource not found409 Conflict: Resource conflict (e.g., duplicate entry)500 Internal Server Error: Server-side error
- POST /api/auth/token
- POST /api/tenants # Create tenant
- GET /api/tenants/:id # Get tenant
- PUT /api/tenants/:id # Update tenant
- GET /api/tenants # List tenants (admin only)
- DELETE /api/tenants/:id # Deactivate tenant
- POST /api/tenants/:tenantId/employees # Add employee
- GET /api/tenants/:tenantId/employees/:id # Get employee
- PUT /api/tenants/:tenantId/employees/:id # Update employee
- GET /api/tenants/:tenantId/employees # List employees (paginated)
- DELETE /api/tenants/:tenantId/employees/:id # Soft delete
- POST /api/tenants/:tenantId/payroll-runs # Create payroll run
- GET /api/tenants/:tenantId/payroll-runs/:id # Get payroll details
- PUT /api/tenants/:tenantId/payroll-runs/:id/approve # Approve payroll
- GET /api/tenants/:tenantId/payroll-runs # List payroll runs
- POST /api/tenants/:tenantId/payroll-runs/:id/calculate # Calculate payroll
- POST /api/tenants/:tenantId/payments # Initiate payment
- GET /api/tenants/:tenantId/payments/:id # Get payment status
- GET /api/tenants/:tenantId/payments # List payments (filterable)
- GET /api/tenants/:tenantId/reports/payroll-summary # Payroll summary
- GET /api/tenants/:tenantId/reports/pay-stub/:empId # Employee pay stub
- GET /api/tenants/:tenantId/reports/attendance # Attendance report
-
POST /api/tenants/:tenantId/ai/insights/payroll-anomalies
-
Request: { "payrollRunId": "...", "threshold": 0.85 }
-
Response: [{ "employeeId": "...", "anomaly": "high deduction", "confidence": 0.92 }]
-
POST /api/tenants/:tenantId/ai/recommendations/optimization
-
Get AI-powered payroll optimization suggestions
Role-Based Access Control (RBAC):
| Role | Permissions |
|---|---|
| admin | Full system access, user management, payroll approval |
| hr_officer | Employee data, attendance, department management |
| payroll_officer | Salary structures, payroll calculations, payment initiation |
| employee | View own pay stubs, attendance, profile |
- Passwords hashed with bcrypt (Spring Security)
- Min. 12 characters, mixed case + numbers + symbols enforced
- Reset tokens expire in 24 hours
- Failed login attempts logged and rate-limited
Payroll Anomaly Detection:
- Identifies unusual salary structures, deductions, or payment patterns
- Confidence scoring: anomalies ranked by severity
- Supports explainability ("Why flagged?")
Payroll Optimization:
- Recommends tax optimization strategies
- Suggests overtime reduction opportunities
- Analyzes allowance allocations
Development Mode (with hot-reload):
./mvnw spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=dev"
Debug Mode (attach debugger on port 5005):
./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"
With Live Database:
docker-compose up -d postgres redis ./mvnw spring-boot:run
Unit Tests (Service Layer):
@ExtendWith(MockitoExtension.class) class PayrollServiceTest { @Mock private EmployeeRepository employeeRepository; @InjectMocks private PayrollService payrollService;
@Test
void testCalculatePayroll_Success() {
// Arrange
Employee emp = new Employee(/* ... */);
when(employeeRepository.findById(any())).thenReturn(Optional.of(emp));
// Act
PayrollDetail detail = payrollService.calculatePayroll(emp.getId());
// Assert
assertNotNull(detail);
assertEquals(BigDecimal.valueOf(50000), detail.getGrossSalary());
}
}
Integration Tests (with TestContainers):
@SpringBootTest @Testcontainers class PayrollIntegrationTest { @Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@Test
void testPayrollRun_E2E() {
// Full payroll cycle test with real DB
}
}
Run Tests:
./mvnw test # All tests ./mvnw test -Dtest=PayrollServiceTest # Single test class ./mvnw verify # With coverage
FROM eclipse-temurin:21-jre-alpine COPY target/multi-tenant-ai-payroll-system-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]
Build & Push:
docker build -t your-registry/payroll-system:1.0.0 . docker push your-registry/payroll-system:1.0.0
Error: org.postgresql.util.PSQLException: Connection refused
Solution: docker-compose up -d postgres Or check if port 5432 is already in use lsof -i :5432
Error: io.lettuce.core.RedisConnectionException
Solution: docker-compose up -d redis redis-cli ping # Should return PONG
Error: Jwt expired
Solution:
- Ensure JWT issuer URI is correctly configured
- Check clock skew (time sync between servers)
- Refresh token if expired
Error: java.lang.OutOfMemoryError: Java heap space
Solution: ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-Xmx2g -Xms1g"
- Spring Boot Docs: https://spring.io/projects/spring-boot
- Spring Data JPA: https://spring.io/projects/spring-data-jpa
- OAuth2 & Spring Security: https://spring.io/guides/gs/securing-web/
- Spring AI (Google GenAI): https://docs.spring.io/spring-ai/reference/
- PostgreSQL JSONB: https://www.postgresql.org/docs/current/datatype-json.html
- Redis Caching: https://redis.io/docs/
- User Stories: User Stories.pdf
- ERD:

For issues, questions, or suggestions:
- GitHub Issues: Report Bug
Last Updated: December 2025
Maintainer: @DroidZeroCodes