Java Notes
Build an Employee Management System with Spring Boot REST API - full CRUD, department management, salary processing, and role-based access.
Project Overview
The Employee Management System is a comprehensive Spring Boot REST API application that manages employee records, department hierarchy, salary processing, attendance tracking, and leave management. It implements full CRUD operations with advanced features like search/filter, pagination, role-based access control, and report generation.
This project is ideal for:
- Java developers learning Spring Boot and REST API development
- Students building portfolio projects for campus placements
- Developers preparing for Spring Boot developer interviews
- Anyone wanting to understand enterprise application architecture
The system supports three user roles (Admin, HR Manager, Employee) with different access levels, implements comprehensive validation, and follows industry-standard design patterns (Repository, Service, DTO).
Features
- Employee CRUD — Create, Read, Update, Delete employee records with validation
- Department Management — CRUD for departments with manager assignment
- Search & Filter — Search employees by name, email, department, designation
- Pagination & Sorting — Efficient data retrieval with configurable page size
- Role-Based Access — Admin (full), HR Manager (read/write), Employee (read own)
- Salary Management — Monthly salary processing with allowances and deductions
- Attendance Tracking — Daily check-in/check-out with work hours calculation
- Leave Management — Apply, approve/reject leave with balance tracking
- Department Hierarchy — Parent-child department structure with org chart
- Employee Onboarding — New hire workflow with document checklist
- Performance Reviews — Annual/quarterly review submission and tracking
- Report Generation — Excel and PDF reports for HR analytics
- Email Notifications — Welcome email, leave approval, salary slip
- Bulk Import — Upload employees via CSV/Excel file
- Audit Trail — Track all modifications with who/when/what
- Profile Photo — Upload and manage employee profile pictures
- Dashboard Stats — Total employees, department-wise count, recent joins
- Designation Management — Configure job titles and bands
- Emergency Contacts — Store employee emergency contact details
- Document Management — Upload and store employee documents (ID, certificates)
Project Structure
Database Schema
employees Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique ID |
| employee_id | VARCHAR(10) | UNIQUE, NOT NULL | Employee code (EMP001) |
| first_name | VARCHAR(50) | NOT NULL | First name |
| last_name | VARCHAR(50) | NOT NULL | Last name |
| VARCHAR(100) | UNIQUE, NOT NULL | Official email | |
| phone | VARCHAR(15) | NOT NULL | Contact number |
| gender | ENUM | NOT NULL | MALE, FEMALE, OTHER |
| date_of_birth | DATE | NOT NULL | Birth date |
| hire_date | DATE | NOT NULL | Joining date |
| department_id | BIGINT | FOREIGN KEY | Department reference |
| designation_id | BIGINT | FOREIGN KEY | Designation reference |
| manager_id | BIGINT | FOREIGN KEY (self) | Reporting manager |
| salary | DECIMAL(12,2) | NOT NULL | Base monthly salary (CTC) |
| status | ENUM | DEFAULT 'ACTIVE' | ACTIVE, ON_LEAVE, RESIGNED, TERMINATED |
| address | VARCHAR(255) | Residential address | |
| city | VARCHAR(50) | City | |
| profile_photo_url | VARCHAR(255) | Profile picture URL | |
| role | ENUM | NOT NULL | ADMIN, HR_MANAGER, EMPLOYEE |
| password | VARCHAR(255) | NOT NULL | BCrypt encrypted password |
| created_at | TIMESTAMP | NOT NULL | Record creation time |
| updated_at | TIMESTAMP | NOT NULL | Last modification time |
departments Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique ID |
| name | VARCHAR(100) | UNIQUE, NOT NULL | Department name |
| code | VARCHAR(10) | UNIQUE, NOT NULL | Short code (IT, HR, FIN) |
| description | VARCHAR(255) | Department description | |
| head_id | BIGINT | FOREIGN KEY | Department head employee |
| parent_department_id | BIGINT | FOREIGN KEY (self) | Parent department |
| location | VARCHAR(100) | Office location | |
| created_at | TIMESTAMP | NOT NULL | Creation time |
attendance Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique ID |
| employee_id | BIGINT | FOREIGN KEY | Employee reference |
| date | DATE | NOT NULL | Attendance date |
| check_in | TIME | Check-in time | |
| check_out | TIME | Check-out time | |
| work_hours | DECIMAL(4,2) | Total hours worked | |
| status | ENUM | NOT NULL | PRESENT, ABSENT, HALF_DAY, ON_LEAVE |
| remarks | VARCHAR(255) | Notes |
leaves Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique ID |
| employee_id | BIGINT | FOREIGN KEY | Employee reference |
| leave_type | ENUM | NOT NULL | CASUAL, SICK, EARNED, MATERNITY |
| start_date | DATE | NOT NULL | Leave start |
| end_date | DATE | NOT NULL | Leave end |
| total_days | INT | NOT NULL | Number of days |
| reason | VARCHAR(500) | NOT NULL | Leave reason |
| status | ENUM | DEFAULT 'PENDING' | PENDING, APPROVED, REJECTED |
| approved_by | BIGINT | FOREIGN KEY | Approver reference |
| applied_on | TIMESTAMP | NOT NULL | Application timestamp |
salaries Table
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | Unique ID |
| employee_id | BIGINT | FOREIGN KEY | Employee reference |
| month | INT | NOT NULL | Salary month (1-12) |
| year | INT | NOT NULL | Salary year |
| basic_salary | DECIMAL(12,2) | NOT NULL | Basic component |
| hra | DECIMAL(10,2) | House Rent Allowance | |
| da | DECIMAL(10,2) | Dearness Allowance | |
| pf_deduction | DECIMAL(10,2) | Provident Fund deduction | |
| tax_deduction | DECIMAL(10,2) | Income Tax (TDS) | |
| net_salary | DECIMAL(12,2) | NOT NULL | Take-home salary |
| paid_on | TIMESTAMP | Payment date | |
| status | ENUM | DEFAULT 'PENDING' | PENDING, PROCESSED, PAID |
Entity Relationships
Key Code Snippets
Employee Entity
package com.wohotech.ems.entity;
import com.wohotech.ems.enums.*;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "employees")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "employee_id", unique = true, nullable = false, length = 10)
private String employeeId; // EMP001, EMP002
@Column(name = "first_name", nullable = false, length = 50)
private String firstName;
@Column(name = "last_name", nullable = false, length = 50)
private String lastName;
@Column(unique = true, nullable = false, length = 100)
private String email;
@Column(nullable = false, length = 15)
private String phone;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Gender gender;
@Column(name = "date_of_birth", nullable = false)
private LocalDate dateOfBirth;
@Column(name = "hire_date", nullable = false)
private LocalDate hireDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "designation_id")
private Designation designation;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "manager_id")
private Employee manager;
@OneToMany(mappedBy = "manager")
private List<Employee> subordinates;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal salary;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
@Builder.Default
private EmployeeStatus status = EmployeeStatus.ACTIVE;
private String address;
private String city;
@Column(name = "profile_photo_url")
private String profilePhotoUrl;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@Column(nullable = false)
private String password;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Helper method
public String getFullName() {
return firstName + " " + lastName;
}
// Calculate years of experience
public int getYearsOfService() {
return java.time.Period.between(hireDate, LocalDate.now()).getYears();
}
}Employee Repository
package com.wohotech.ems.repository;
import com.wohotech.ems.entity.Employee;
import com.wohotech.ems.enums.EmployeeStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long>,
JpaSpecificationExecutor<Employee> {
Optional<Employee> findByEmployeeId(String employeeId);
Optional<Employee> findByEmail(String email);
boolean existsByEmail(String email);
Page<Employee> findByDepartmentId(Long departmentId, Pageable pageable);
Page<Employee> findByStatus(EmployeeStatus status, Pageable pageable);
@Query("SELECT e FROM Employee e WHERE " +
"LOWER(e.firstName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
"LOWER(e.lastName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
"LOWER(e.email) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
"e.employeeId LIKE CONCAT('%', :keyword, '%')")
Page<Employee> searchEmployees(@Param("keyword") String keyword, Pageable pageable);
@Query("SELECT e FROM Employee e WHERE e.hireDate BETWEEN :startDate AND :endDate")
List<Employee> findNewJoinees(@Param("startDate") LocalDate start,
@Param("endDate") LocalDate end);
@Query("SELECT e.department.name, COUNT(e) FROM Employee e " +
"WHERE e.status = 'ACTIVE' GROUP BY e.department.name")
List<Object[]> getEmployeeCountByDepartment();
@Query("SELECT COUNT(e) FROM Employee e WHERE e.status = :status")
long countByStatus(@Param("status") EmployeeStatus status);
List<Employee> findByManagerId(Long managerId);
@Query("SELECT AVG(e.salary) FROM Employee e WHERE e.department.id = :deptId")
Double getAverageSalaryByDepartment(@Param("deptId") Long departmentId);
}Employee Service Implementation
Employee Controller
package com.wohotech.ems.controller;
import com.wohotech.ems.dto.request.EmployeeCreateRequest;
import com.wohotech.ems.dto.request.EmployeeUpdateRequest;
import com.wohotech.ems.dto.response.DashboardResponse;
import com.wohotech.ems.dto.response.EmployeeResponse;
import com.wohotech.ems.dto.response.PagedResponse;
import com.wohotech.ems.service.EmployeeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/employees")
@RequiredArgsConstructor
@Tag(name = "Employee Management", description = "CRUD operations for employees")
public class EmployeeController {
private final EmployeeService employeeService;
@PostMapping
@PreAuthorize("hasRole('ADMIN') or hasRole('HR_MANAGER')")
@Operation(summary = "Create a new employee")
public ResponseEntity<EmployeeResponse> createEmployee(
@Valid @RequestBody EmployeeCreateRequest request) {
return new ResponseEntity<>(employeeService.createEmployee(request), HttpStatus.CREATED);
}
@GetMapping
@Operation(summary = "Get all employees with pagination")
public ResponseEntity<PagedResponse<EmployeeResponse>> getAllEmployees(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "hireDate") String sortBy,
@RequestParam(defaultValue = "desc") String sortDir) {
return ResponseEntity.ok(employeeService.getAllEmployees(page, size, sortBy, sortDir));
}
@GetMapping("/{id}")
@Operation(summary = "Get employee by ID")
public ResponseEntity<EmployeeResponse> getEmployeeById(@PathVariable Long id) {
return ResponseEntity.ok(employeeService.getEmployeeById(id));
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or hasRole('HR_MANAGER')")
@Operation(summary = "Update employee details")
public ResponseEntity<EmployeeResponse> updateEmployee(
@PathVariable Long id,
@Valid @RequestBody EmployeeUpdateRequest request) {
return ResponseEntity.ok(employeeService.updateEmployee(id, request));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Delete (soft) employee")
public ResponseEntity<String> deleteEmployee(@PathVariable Long id) {
employeeService.deleteEmployee(id);
return ResponseEntity.ok("Employee deleted successfully");
}
@GetMapping("/search")
@Operation(summary = "Search employees by keyword")
public ResponseEntity<PagedResponse<EmployeeResponse>> searchEmployees(
@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(employeeService.searchEmployees(keyword, page, size));
}
@GetMapping("/department/{departmentId}")
@Operation(summary = "Get employees by department")
public ResponseEntity<PagedResponse<EmployeeResponse>> getByDepartment(
@PathVariable Long departmentId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return ResponseEntity.ok(employeeService.getByDepartment(departmentId, page, size));
}
@GetMapping("/dashboard")
@PreAuthorize("hasRole('ADMIN') or hasRole('HR_MANAGER')")
@Operation(summary = "Get dashboard statistics")
public ResponseEntity<DashboardResponse> getDashboard() {
return ResponseEntity.ok(employeeService.getDashboardStats());
}
}API Endpoints
| Method | URL | Description | Auth |
|---|---|---|---|
| POST | /api/v1/employees | Create employee | Admin/HR |
| GET | /api/v1/employees | List all (paginated) | Any |
| GET | /api/v1/employees/{id} | Get by ID | Any |
| PUT | /api/v1/employees/{id} | Update employee | Admin/HR |
| DELETE | /api/v1/employees/{id} | Soft delete | Admin |
| GET | /api/v1/employees/search?keyword= | Search employees | Any |
| GET | /api/v1/employees/department/{id} | By department | Any |
| GET | /api/v1/employees/dashboard | Dashboard stats | Admin/HR |
| POST | /api/v1/departments | Create department | Admin |
| GET | /api/v1/departments | List departments | Any |
| POST | /api/v1/attendance/check-in | Mark check-in | Employee |
| POST | /api/v1/attendance/check-out | Mark check-out | Employee |
| GET | /api/v1/attendance/{empId}/monthly | Monthly attendance | Any |
| POST | /api/v1/leaves/apply | Apply for leave | Employee |
| PUT | /api/v1/leaves/{id}/approve | Approve leave | Manager |
| PUT | /api/v1/leaves/{id}/reject | Reject leave | Manager |
| GET | /api/v1/leaves/balance/{empId} | Leave balance | Employee |
| POST | /api/v1/salary/process | Process monthly salary | Admin |
| GET | /api/v1/salary/{empId}/slip/{month}/{year} | Salary slip | Employee |
| GET | /api/v1/reports/employees/excel | Download Excel | Admin/HR |
| POST | /api/v1/auth/login | Login | No |
Sample Request & Response
POST /api/v1/employees
Request:
{
"firstName": "Rahul",
"lastName": "Sharma",
"email": "rahul.sharma@company.com",
"phone": "9876543210",
"gender": "MALE",
"dateOfBirth": "1995-03-15",
"hireDate": "2024-06-01",
"departmentId": 2,
"managerId": 5,
"salary": 75000.00,
"address": "123 MG Road, Bangalore",
"city": "Bangalore"
}Response (201 Created):
{
"id": 42,
"employeeId": "EMP042",
"firstName": "Rahul",
"lastName": "Sharma",
"email": "rahul.sharma@company.com",
"phone": "9876543210",
"department": "Engineering",
"designation": null,
"manager": "Priya Patel",
"salary": 75000.00,
"status": "ACTIVE",
"hireDate": "2024-06-01",
"createdAt": "2024-06-12T10:00:00"
}How to Run
Prerequisites
- Java 17+, Maven 3.9+, MySQL 8.0
Step-by-Step
Step 1: Clone
git clone https://github.com/wohotech/employee-management-system.git
cd employee-management-systemStep 2: Create Database
CREATE DATABASE ems_db;
CREATE USER 'ems_user'@'localhost' IDENTIFIED BY 'ems@123';
GRANT ALL PRIVILEGES ON ems_db.* TO 'ems_user'@'localhost';Step 3: Configure application.yml
Step 4: Build & Run
mvn clean install
mvn spring-boot:runStep 5: Access
- API:
http://localhost:8080/api/v1 - Swagger:
http://localhost:8080/swagger-ui.html - Default Admin:
admin@company.com / Admin@123
Screenshots Description
- Swagger UI — All employee endpoints with request/response schemas
- Create Employee — POST request with validation error examples
- Employee List — Paginated response with sorting options
- Search Results — Keyword search matching name and email
- Dashboard Stats — Total employees, department distribution chart data
- Attendance Report — Monthly calendar view with present/absent/leave markers
- Leave Management — Leave application form and approval workflow
- Salary Slip — Monthly salary breakdown with allowances and deductions
Future Enhancements
- Biometric Integration — Fingerprint-based attendance marking
- Org Chart Visualization — Interactive organizational hierarchy
- Employee Self-Service Portal — React frontend for employees
- Training Management — Track employee training and certifications
- Recruitment Module — Job postings, applications, interview scheduling
- Expense Management — Submit and approve expense claims
- Multi-tenant Support — Serve multiple companies from one instance
- Mobile App — Flutter-based mobile attendance and leave app
- Analytics Dashboard — Attrition prediction, salary benchmarking
- Integration APIs — Connect with HRMS tools like SAP, Workday
Interview Questions
Q1: How do you implement pagination and sorting in Spring Boot?
Answer: Spring Data JPA provides Pageable interface. In the controller, we accept page, size, sortBy, sortDir parameters. We create PageRequest.of(page, size, Sort.by(sortBy).ascending()) and pass it to repository methods that return Page<Entity>. The response includes content, totalElements, totalPages, hasNext. This ensures we never load all records into memory — only the requested page is fetched with SQL LIMIT/OFFSET.
Q2: Why use soft delete instead of hard delete for employees?
Answer: Hard delete causes referential integrity issues (attendance records, salary history point to deleted employee). Soft delete (setting status to RESIGNED/TERMINATED) preserves historical data for audit/compliance, allows data recovery, and maintains FK relationships. We use @Where(clause = "status != 'RESIGNED'") annotation or custom queries to exclude soft-deleted records from normal queries.
Q3: How do you handle the DTO pattern in this project?
Answer: We separate the API layer from the database layer using DTOs. EmployeeCreateRequest has only fields needed for creation (with @Valid annotations). EmployeeResponse excludes sensitive fields (password) and flattens relationships (shows department name, not the full Department object). MapStruct auto-generates the mapping code at compile time, which is faster than reflection-based mappers like ModelMapper.
Q4: How is the salary calculation implemented?
Answer: Basic = 40% of CTC, HRA = 50% of Basic, DA = 10% of Basic, Special Allowance = remaining. Deductions: PF = 12% of Basic (capped at ₹1800), Professional Tax = ₹200, TDS calculated based on slab. net_salary = (basic + hra + da + special) - (pf + pt + tds). The SalaryService.processMonthly() method iterates all active employees, calculates components, and creates Salary records in bulk.
Q5: How do you secure the APIs with role-based access?
Answer: Spring Security with JWT: (1) User logs in, receives JWT token, (2) Token contains user role claim, (3) SecurityFilterChain validates token on each request, (4) @PreAuthorize("hasRole('ADMIN')") on controller methods restricts access, (5) Custom AccessDeniedHandler returns 403 with proper message. We also implement method-level security so employees can only view their own data.
Q6: How would you implement bulk employee import from Excel?
Answer: We use Apache POI to read .xlsx files: (1) Controller accepts MultipartFile, (2) Service reads each row, validates data (email format, required fields, no duplicates), (3) Collects errors per row without failing entire import, (4) Uses saveAll() with batch processing for valid rows, (5) Returns summary: {imported: 45, failed: 3, errors: [{row: 7, field: "email", message: "duplicate"}]}. We set spring.jpa.properties.hibernate.jdbc.batch_size=50 for efficient batch inserts.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Employee Management System.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, projects, employee, management
Related Java Master Course Topics