A production-grade payroll management REST API built with Spring Boot. It models Philippine statutory payroll rules (SSS, PhilHealth, Pag-IBIG, and withholding tax) alongside employee records, attendance, overtime, leave, and full payroll runs.
This project serves as a demonstration of solid backend engineering: layered domain architecture, stateless JWT security, database migrations, batch processing, and a clean CI/CD + container deployment pipeline to AWS EC2.
Live:
https://mysweldo-api.iodsky.com· Swagger UI:https://mysweldo-api.iodsky.com/api/swagger-ui.html
| Concern | Technology |
|---|---|
| Language / Runtime | Java 21 |
| Framework | Spring Boot 3.5.6 |
| Build | Gradle (Kotlin DSL, version catalog) |
| Persistence | Spring Data JPA + PostgreSQL 16 |
| Migrations | Flyway |
| Security | Spring Security + JWT (access + refresh, HTTP-only cookies) |
| Batch | Spring Batch |
| API Docs | springdoc-openapi (Swagger UI) |
| Container | Docker (multi-stage, eclipse-temurin) |
- Employee management — departments, positions, employment types, pay types, salaries, government IDs.
- Time & attendance — clock-in/clock-out, attendance views, overtime requests.
- Leave management — leave credits, leave requests with approval flow.
- Statutory tables — SSS, PhilHealth, Pag-IBIG contribution tables and tax brackets.
- Payroll engine — pay-basis strategies (hourly/daily/monthly), statutory deduction & contribution computation, payroll runs.
- Security — role-based access control, stateless JWT auth with refresh tokens.
- Bulk import — on-demand Spring Batch jobs for CSV uploads (employees & users).
- Standardized responses —
ApiResponseenvelope with pagination metadata and centralized exception handling. - Soft delete & auditing — every entity extends a common
BaseModel(timestamps, optimistic locking, soft delete).
The codebase follows a strict layered architecture, with each business domain owning the same vertical slice:
<Domain>Controller.java # REST endpoints (returns ResponseFactory envelopes)
<Domain>Service.java # business logic (@Service, @RequiredArgsConstructor)
<Domain>Repository.java # Spring Data JPA
<Domain>Entity.java # JPA entity extends BaseModel
<Domain>Request.java # inbound DTO
<Domain>Dto.java # outbound DTO
<Domain>Mapper.java # Entity <-> DTO conversion (also Request -> Entity)
Domains: attendance, benefit, contribution, deduction, department, employee, leave, overtime, pagIbig, philhealth, position, security, sss, tax, and the payroll engine.
BaseModel— superclass of every entity; providescreatedAt/updatedAt,createdBy/lastModifiedBy(JPA auditing),version(optimistic locking), anddeletedAt(soft delete via@SQLRestriction).response/—ResponseFactorybuilds a consistentApiResponseenvelope; errors flow throughGlobalExceptionHandler.- Services signal failures with
ResponseStatusException.
Payroll logic is deliberately decomposed, not centralized:
payroll/core—PayrollCalculator+PayrollItemAssemblercompose statutory deductions/contributions and line items.payroll/strategy—PayBasisStrategyFactoryselects the hourly/daily/monthly pay-basis strategy;StandardPayrollComputationStrategyorchestrates computation.payroll/run—PayrollRunServiceexecutes full payroll runs across active employees.
- JWT issued at
/auth/loginand/auth/refresh; refresh tokens are delivered as HTTP-only cookies. - Only
/auth/**,/docs/**, and/swagger-ui/**are public; everything else requires a valid Bearer token. - Role-based access is enforced via Spring method security (
@EnableMethodSecurity).
- Spring Batch jobs are disabled on startup (
spring.batch.job.enabled: false) and launched on demand viabatch/BatchController. - CSV uploads are capped at 10 MB and stored under
uploads/.
- Java 21
- Docker (for the local PostgreSQL + pgAdmin)
.envfile (see below)
cp .env.template .envFill in the DB credentials, JWT secret, and expiration values. .env is loaded through spring.config.import, not Gradle — any new environment variable must also be added to .env.template.
docker compose -f db.compose.yml up -dThis starts PostgreSQL 16 on localhost:5432 and pgAdmin at :5050.
./run.shrun.sh sources .env and runs ./gradlew bootRun with the local profile. The server listens on the port defined by PORT (default 8001) under the /api context path.
- Swagger UI:
http://localhost:8001/api/swagger-ui.html - OpenAPI JSON:
http://localhost:8001/api/docs
# All tests
./gradlew clean test
# Single test class / method
./gradlew test --tests "com.iodsky.mysweldo.benefit.BenefitServiceTest"
./gradlew test --tests "com.iodsky.mysweldo.benefit.BenefitServiceTest.methodName"Tests use JUnit 5 + AssertJ, are named <Domain>ServiceTest, and live under src/test/java/com/iodsky/mysweldo/<domain>/. Repository/view stubs are colocated with the tests.
- Flyway migrations live in
src/main/resources/db/migration/asV{n}__description.sql. - Never modify an existing migration — both profiles run
ddl-auto: validateand Flyway checksums will fail. Add a newV{n+1}__...sqlinstead.
| Profile | Activation | Database |
|---|---|---|
local |
default via ./run.sh |
localhost:5432 from .env (LOCAL_DB_*) |
prod |
set at CI/CD deploy | Cloud PostgreSQL from .env (CLOUD_DB_*) |
Key environment variables (see .env.template): PORT, LOCAL_DB_*, CLOUD_DB_*, JWT_SECRET_KEY, JWT_ACCESS_EXPIRATION, JWT_REFRESH_EXPIRATION, JWT_COOKIE_*, CORS_ALLOWED_ORIGINS.
The API is containerized and deployed to an EC2 instance, exposed at https://mysweldo-api.iodsky.com through a Traefik reverse proxy with automatic Let's Encrypt TLS.
The CI pipeline builds a multi-stage Docker image and pushes it to GitHub Container Registry:
./gradlew clean bootJar
docker build -t ghcr.io/iodsky/mysweldo-api:latest .
docker push ghcr.io/iodsky/mysweldo-api:latestdocker compose -f mysweldo-api.compose.yml up -dThe compose file:
- Runs the
ghcr.io/iodsky/mysweldo-api:latestimage on port8001. - Loads configuration from
.env(pointCLOUD_DB_*at the production PostgreSQL). - Joins an external
traefiknetwork. - Registers Traefik routing for
mysweldo-api.iodsky.comover TLS (Let's Encrypt resolverle).
This requires a Traefik instance running on the host with the traefik network and le certresolver already configured, plus a DNS A record for mysweldo-api.iodsky.com pointing at the EC2 instance.
.github/workflows/ci-cd.yml:
- Runs
./gradlew clean teston every PR tomaster/develop. - On
master, builds the boot jar, builds and pushes the Docker image to GHCR.
src/main/java/com/iodsky/mysweldo/
├── common/ # BaseModel, response envelopes, exception handling, config
├── <domain>/ # Controller, Service, Repository, Entity, Request, Dto, Mapper
├── payroll/ # core / strategy / run sub-packages
├── security/ # auth, jwt, role, user
├── batch/ # import jobs (employees, users)
└── Application.java # entry point
src/main/resources/
├── application.yml # shared config
├── application-local.yml # local profile
├── application-prod.yml # production profile
└── db/migration/ # Flyway migrations