A comprehensive Spring Boot backend application for a multi-tenant SaaS platform with project and task management capabilities.
- Java 17 or higher
- PostgreSQL 12 or higher
- Maven 3.6 or higher
# Create database
createdb workhub
# Or using psql
psql -U postgres
CREATE DATABASE workhub;Update src/main/resources/application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/workhub
username: postgres
password: postgres# Build and run
mvn clean spring-boot:runThe application will start on http://localhost:8080
The application automatically creates test data on startup:
Admin User:
- Email:
admin@demo.com - Password:
admin123 - Role: TENANT_ADMIN
Regular User:
- Email:
user@demo.com - Password:
user123 - Role: TENANT_USER
POST /api/auth/login
Content-Type: application/json
{
"email": "admin@demo.com",
"password": "admin123"
}Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"type": "Bearer",
"userId": 1,
"email": "admin@demo.com",
"tenantId": 1,
"role": "TENANT_ADMIN",
"expiresIn": 86400000
}GET /api/auth/me
Authorization: Bearer <token>POST /api/projects
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Project Alpha",
"description": "A new project",
"projectKey": "ALPHA"
}GET /api/projects
Authorization: Bearer <token>GET /api/projects/{id}
Authorization: Bearer <token>POST /api/projects/{id}/tasks
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "Implement feature",
"description": "Feature description",
"priority": "HIGH",
"estimatedHours": 8
}PATCH /api/tasks/{id}
Authorization: Bearer <token>
Content-Type: application/json
{
"status": "IN_PROGRESS",
"actualHours": 4
}# 1. Login
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@demo.com","password":"admin123"}' \
| jq -r '.token')
echo "Token: $TOKEN"
# 2. Get current user
curl -X GET http://localhost:8080/api/auth/me \
-H "Authorization: Bearer $TOKEN"
# 3. Create a project
PROJECT_ID=$(curl -s -X POST http://localhost:8080/api/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My First Project",
"description": "Testing the API",
"projectKey": "TEST"
}' | jq -r '.id')
echo "Created Project ID: $PROJECT_ID"
# 4. Get all projects
curl -X GET http://localhost:8080/api/projects \
-H "Authorization: Bearer $TOKEN"
# 5. Get project by ID
curl -X GET http://localhost:8080/api/projects/$PROJECT_ID \
-H "Authorization: Bearer $TOKEN"
# 6. Create a task
TASK_ID=$(curl -s -X POST http://localhost:8080/api/projects/$PROJECT_ID/tasks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Implement authentication",
"description": "Add JWT authentication",
"priority": "HIGH",
"estimatedHours": 16
}' | jq -r '.id')
echo "Created Task ID: $TASK_ID"
# 7. Update task status
curl -X PATCH http://localhost:8080/api/tasks/$TASK_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "IN_PROGRESS",
"actualHours": 8
}'
# 8. Get task
curl -X GET http://localhost:8080/api/tasks/$TASK_ID \
-H "Authorization: Bearer $TOKEN"| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /api/auth/login |
Login and get JWT token | No |
| GET | /api/auth/me |
Get current user profile | Yes |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| POST | /api/projects |
Create project | Yes |
| GET | /api/projects |
Get all projects | Yes |
| GET | /api/projects/{id} |
Get project by ID | Yes |
| POST | /api/projects/{id}/tasks |
Create task for project | Yes |
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
| GET | /api/tasks/{id} |
Get task by ID | Yes |
| PATCH | /api/tasks/{id} |
Update task (partial) | Yes |
| DELETE | /api/tasks/{id} |
Delete task | Yes |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/test/transaction/rollback |
Test transaction rollback |
| GET | /api/test/transaction/verify |
Verify rollback |
| GET | /api/test/transaction/instructions |
Get test instructions |
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@demo.com",
"password": "admin123"
}'- Method: POST
- URL:
http://localhost:8080/api/auth/login - Headers:
Content-Type: application/json - Body (raw JSON):
{
"email": "admin@demo.com",
"password": "admin123"
}{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInRlbmFudElkIjoxLCJyb2xlIjoiVEVOQU5UX0FETUlOIiwic3ViIjoiYWRtaW5AZGVtby5jb20iLCJpYXQiOjE3MDUzMjAwMDAsImV4cCI6MTcwNTQwNjQwMH0.xxx",
"type": "Bearer",
"userId": 1,
"email": "admin@demo.com",
"tenantId": 1,
"role": "TENANT_ADMIN",
"expiresIn": 86400000
}Add the token to all subsequent requests:
curl -X GET http://localhost:8080/api/projects \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."# Clean and run
mvn clean spring-boot:run
# Run with dev profile
mvn spring-boot:run -Dspring-boot.run.profiles=dev
# Run with specific port
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=9090# Build JAR
mvn clean package
# Run JAR
java -jar target/workhub-0.0.1-SNAPSHOT.jar
# Run with profile
java -jar target/workhub-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod# Build image
docker build -t workhub:latest .
# Run container
docker run -p 8080:8080 \
-e DATABASE_URL=jdbc:postgresql://host.docker.internal:5432/workhub \
-e DATABASE_USERNAME=postgres \
-e DATABASE_PASSWORD=postgres \
workhub:latest# Start all services (app + database)
docker-compose up -d
# View logs
docker-compose logs -f
# Stop services
docker-compose downFile: src/main/resources/application.yml
spring:
application:
name: workhub
datasource:
url: jdbc:postgresql://localhost:5432/workhub
username: postgres
password: postgres
jpa:
hibernate:
ddl-auto: update # Creates/updates tables automatically
show-sql: true # Shows SQL in logs
server:
port: 8080
jwt:
secret: ${JWT_SECRET:your-secret-key}
expiration: 86400000 # 24 hours# Database
export DATABASE_URL=jdbc:postgresql://localhost:5432/workhub
export DATABASE_USERNAME=postgres
export DATABASE_PASSWORD=postgres
# JWT
export JWT_SECRET=your-secret-key-here
# Run
mvn spring-boot:runβββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Controller Layer β
β - REST endpoints β
β - Request/Response handling β
β - Input validation (@Valid) β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Service Layer β
β - Business logic β
β - Transaction management (@Transactional) β
β - Tenant isolation (TenantContext) β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Repository Layer β
β - Data access (Spring Data JPA) β
β - Tenant-filtered queries β
β - Database operations β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Request with JWT
β
TenantContextFilter extracts tenantId from JWT
β
TenantContext.setTenantId(tenantId)
β
Service uses TenantContext.getTenantId()
β
Repository filters by tenantId
β
Only tenant's data returned
- Start application:
mvn spring-boot:run - Login: Get JWT token
- Test endpoints: Use curl or Postman
- Verify: Check database
# Test transaction rollback
curl -X POST http://localhost:8080/api/test/transaction/rollback \
-H "Authorization: Bearer $TOKEN"
# Expected: 500 error (intentional)
# Verify nothing saved
curl -X GET "http://localhost:8080/api/test/transaction/verify?projectId=X&taskId=Y" \
-H "Authorization: Bearer $TOKEN"
# Expected: rollbackSuccessful: true| Technology | Version | Purpose |
|---|---|---|
| Java | 17 | Programming language |
| Spring Boot | 3.3.5 | Application framework |
| Spring Data JPA | 3.3.5 | Data access |
| Spring Security | 3.3.5 | Authentication & authorization |
| PostgreSQL | 12+ | Database |
| Lombok | Latest | Code generation |
| JWT (jjwt) | 0.11.5 | Token authentication |
| Maven | 3.6+ | Build tool |
- Token-based authentication
- Includes
tenantIdandrolein claims - Automatic tenant context setup
- 24-hour token expiration
- All operations automatically filtered by tenant
- No cross-tenant access possible
- ThreadLocal-based tenant context
- Automatic cleanup after request
- BCrypt password hashing
- No plain text passwords stored
- Secure password validation
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@demo.com",
"password": "admin123"
}'curl -X POST http://localhost:8080/api/projects \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Project",
"description": "Project description",
"projectKey": "PROJ"
}'curl -X GET http://localhost:8080/api/projects \
-H "Authorization: Bearer $TOKEN"curl -X POST http://localhost:8080/api/projects/1/tasks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Implement feature",
"description": "Feature description",
"priority": "HIGH",
"estimatedHours": 8
}'curl -X PATCH http://localhost:8080/api/tasks/1 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "IN_PROGRESS",
"actualHours": 4
}'- β Complete tenant isolation
- β Automatic tenant context from JWT
- β No cross-tenant data access
- β ThreadLocal-based context management
- β Tenant: Organization/company
- β User: Users with roles (TENANT_ADMIN, TENANT_USER)
- β Project: Projects with status tracking
- β Task: Tasks with priority, status, and assignments
- β JWT authentication
- β BCrypt password hashing
- β Role-based access control
- β Tenant-aware security filter
- β Spring Data JPA repositories
- β Tenant-filtered queries
- β Transaction management
- β Optimistic locking (@Version)
- β RESTful endpoints
- β DTO pattern (not exposing entities)
- β Input validation (@Valid)
- β Consistent error responses
- β Global exception handling
src/main/java/com/workhub/
βββ entity/ # JPA entities
β βββ Tenant.java
β βββ User.java
β βββ Project.java
β βββ Task.java
βββ dto/ # Data Transfer Objects
β βββ LoginRequest.java
β βββ LoginResponse.java
β βββ CreateProjectRequest.java
β βββ ProjectResponse.java
β βββ CreateTaskRequest.java
β βββ TaskResponse.java
β βββ UpdateTaskRequest.java
βββ repository/ # Spring Data JPA repositories
β βββ TenantRepository.java
β βββ UserRepository.java
β βββ ProjectRepository.java
β βββ TaskRepository.java
βββ service/ # Business logic
β βββ ProjectService.java
β βββ TaskService.java
βββ controller/ # REST controllers
β βββ AuthController.java
β βββ ProjectController.java
β βββ TaskController.java
βββ security/ # Security components
β βββ SecurityConfig.java
β βββ JwtUtil.java
β βββ JwtAuthenticationFilter.java
β βββ CustomUserDetails.java
β βββ CustomUserDetailsService.java
β βββ TenantContext.java
β βββ TenantContextFilter.java
βββ exception/ # Exception handling
β βββ GlobalExceptionHandler.java
βββ config/ # Configuration
β βββ DataInitializer.java
βββ WorkHubApplication.java
# Clean build
mvn clean install
# Skip tests
mvn clean install -DskipTests
# Run tests only
mvn test# Development mode (with auto-reload)
mvn spring-boot:run
# With debug
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"# Connect to database
psql -U postgres -d workhub
# View tables
\dt
# View projects
SELECT * FROM projects;
# View tasks
SELECT * FROM tasks;
# View users
SELECT * FROM users;Check:
- PostgreSQL is running
- Database
workhubexists - Database credentials are correct in
application.yml
Solution:
- Login to get a valid JWT token
- Include token in Authorization header:
Bearer <token>
Solution:
- Verify resource exists
- Check that resource belongs to your tenant
- Verify resource ID is correct
Solution:
- Check request body format
- Ensure all required fields are present
- Verify field constraints (min/max length, etc.)
tenants- Organizations/companiesusers- Users belonging to tenantsprojects- Projects belonging to tenantstasks- Tasks belonging to projects
Tenant (1) βββ (N) User
Tenant (1) βββ (N) Project
Project (1) βββ (N) Task
User (1) βββ (N) Project (as creator)
User (1) βββ (N) Task (as assignee)
All tables have indexes on:
tenant_id- For efficient tenant filtering- Foreign keys - For join performance
- Status fields - For filtering queries
- β Clean Architecture - Layered design
- β DTO Pattern - Separate API contracts from entities
- β
Validation - Input validation with
@Valid - β
Transaction Management -
@Transactionalfor consistency - β Exception Handling - Global exception handler
- β Security - JWT authentication with tenant isolation
- β Logging - Structured logging with SLF4J
- β Separation of Concerns - Clear layer boundaries
This project is licensed under the MIT License.
| Password | Role | Tenant | |
|---|---|---|---|
| admin@demo.com | admin123 | TENANT_ADMIN | Demo Company |
| user@demo.com | user123 | TENANT_USER | Demo Company |
After starting the application:
- β Login with test account
- β Create a project
- β Create tasks for the project
- β Update task status
- β Test transaction rollback
For issues or questions, check the documentation files:
TRANSACTION_ROLLBACK_TEST.md- Transaction testing guideSERVICE_LAYER_DOCUMENTATION.md- Service layer detailsTENANT_CONTEXT_USAGE.md- Tenant context usageREST_API_DOCUMENTATION.md- Complete API reference
All API errors now use a single JSON shape:
{
"timestamp": "2026-05-16T16:20:47.126Z",
"status": 403,
"error": "Forbidden",
"message": "Access denied",
"path": "/api/projects",
"correlationId": "6f844d3c-d11d-4d4d-8664-70b5e0bfec92",
"details": []
}TENANT_ADMINrequired for:POST /api/projects/**PATCH /api/tasks/**DELETE /api/tasks/**POST|PUT|DELETE /api/users/**/api/v1/tenants/**
TENANT_USERcan access authenticated read endpoints but cannot perform admin mutations.
- Tenant context is extracted in one trusted place:
JwtAuthenticationFilter. - Tenant context is cleared on every request (
finallyblock). - Resource access uses tenant-safe repository methods such as:
findByIdAndTenantId(...)existsByEmailAndTenantId(...)findByTenantId(...)
# Unauthorized request -> 401 + unified error payload
curl -i http://localhost:8080/api/projects
# Login as TENANT_USER
USER_TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"user@demo.com","password":"user123"}' | jq -r '.token')
# Forbidden mutation for TENANT_USER -> 403
curl -i -X POST http://localhost:8080/api/projects \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Should Fail","description":"RBAC check","projectKey":"DENY"}'Phase 2 introduces asynchronous report workflow, RabbitMQ reliability, idempotent consumer processing, and production observability.
Use the following documents for grading and technical defense:
TENANT-ISOLATION-PROOF.md- Tenant isolation proof strategy and evidence checklist.OBSERVABILITY.md- Actuator/Micrometer setup, structured logging, and correlation tracing validation.async-workflow.md- End-to-end async event architecture and reliability model.RABBITMQ-SETUP-GUIDE.md- Local and operational RabbitMQ setup and verification steps.POSTMAN-COLLECTION-STRUCTURE-PHASE2.md- Recommended Postman folder structure for defense demo.TROUBLESHOOTING-PHASE2.md- Failure patterns, diagnostics, and recovery guidance.TESTPLAN.md- Requirement-to-test matrix with evidence mapping.
- Async reporting:
POST /api/projects/{id}/generate-reportGET /api/jobs/{jobId}GET /api/jobs?status=...
- Observability:
GET /actuator/healthGET /actuator/health/readinessGET /actuator/health/livenessGET /actuator/metrics
- Tenant-safe async processing using tenant-scoped repository queries.
ReportGenerationEventwith correlation propagation across producer and consumer.- Persistent idempotency ledger via
ProcessedMessage(eventIduniqueness). - Duplicate message prevention and retry-safe consumer semantics.
- Durable failure recording with explicit transactional boundaries (
REQUIRES_NEW). - Global unique email identity model to prevent authentication ambiguity.
- Actuator health endpoints public, metrics endpoints protected.