Project Name: Employee Management System (EMS)

Project Description  
Build a RESTful API for managing employees using Spring Boot, JPA, H2 (in-memory DB), validation, global exception handling, and Actuator monitoring.

Requirements / Tasks to Complete

1. Setup & Basic Structure
   - Create Spring Boot project (Maven, Java 17 or 25).
   - Add dependencies: spring-boot-starter-web, spring-boot-starter-data-jpa, h2, spring-boot-starter-validation, spring-boot-starter-actuator, spring-boot-devtools.
   - Configure `application.properties` (H2 console, actuator exposure).

2. Employee Entity
   - `@Entity` class: Employee (id, name, salary, department, email).
   - Use `@NotBlank`, `@Positive`, `@Email`, `@NotNull` for validation.

3. Employee DTOs
   - `EmployeeRequestDto` (for POST/PUT) — with validation annotations.
   - `EmployeeResponseDto` (for GET) — no sensitive fields.

4. EmployeeRepository
   - Extend `JpaRepository<Employee, Long>`.
   - Add custom methods: `findByNameContaining`, `findBySalaryGreaterThan`, `findByDepartment`.

5. EmployeeService
   - CRUD methods with `@Transactional`.
   - Throw custom exceptions (e.g., `EmployeeNotFoundException`, `DuplicateEmailException`).

6. EmployeeController (Full CRUD)
   - GET /employees — list all with pagination & sorting (`Pageable`).
   - GET /employees/{id} — single employee.
   - POST /employees — create with `@Valid`.
   - PUT /employees/{id} — update.
   - DELETE /employees/{id} — delete.

7. Validation & Error Handling
   - Use `@Valid` on DTOs.
   - Global `@RestControllerAdvice` to handle `MethodArgumentNotValidException` and custom exceptions.
   - Return consistent `ErrorResponse` DTO.

8. Actuator Monitoring
   - Expose health, info, metrics.
   - Add custom info (app version, build date).
   - Optional: Custom health indicator (e.g., check DB connection).

9. Bonus (if time)
   - Add `@Query` for advanced search.
   - Test endpoints with Postman/curl.

Deliverable (End Goal)

- Full working REST API for Employee CRUD.
- GitHub repo with commits for each feature.
- Test with Postman (all endpoints, validation errors, exceptions)

_________________________________________________________________________ 

GitHub URLemployee-management-system🔗 

Topics covered

Spring Boot JPA + CRUD 🔗
Spring Boot Actuator + Exception Handling 🔗
Spring Boot Profiles + Testing Basics 🔗

Features

=> Full CRUD operations for employees
=> Batch create multiple employees
=> Search/Filter employees (by name, department, min salary, etc)
=> Pagination & sorting support
=> DTO pattern (No entity exposure)
=> Bean validation (@Valid, @NotBlank, @Positive, @Email)
=> Global Exception handling with custom error responses 
=> Actuator endpoints for monitoring (health, info, metrics)
=> Manual mapping (to avoid external model mapper library issues)

Project Structure 🔗

- employee-management-system/
- ├── pom.xml ← Maven build file (core of Spring Boot project)
- ├── README.md ← Main documentation
- ├── samples/ ← Sample payload json files folder for documentation purpose
- └── src/main/java/
- └── com/
- └── example/
- └── EMS/
- ├── EmsApplication.java ← Main Spring Boot entry point
- ├── config/ ← Configuration classes
- │ - └── DataBaseHealthIndicator.java
- │ - └── GlobalExceptionHandler.java
- │ - └── ModelMapperConfig.java
- ├── controller/ ← REST Controllers
- │ - └── EmployeeController.java
- ├── model/
- │ - ├── dto/ ← Data Transfer Objects
- │ - ├ - ├── EmployeeRequestDto.java
- │ - ├ - ├── EmployeeResponseDto.java
- │ - ├ - └── ErrorResponse.java
- │ - └── entity/ ← JPA Entities
- │ - └── Employee.java
- ├── exception/ ← Custom exceptions
- │ - ├── DuplicateEmailException.java
- │ - └── ResourceNotFoundException.java
- ├── repository/ ← Spring Data JPA Repositories
- │ - └── EmployeeRepository.java
- ├── service/ ← Business logic layer
- │ - ├── impl/
- │ - │ - └── EmployeeServiceImpl.java
- │ - └── EmployeeService.java
- └── util/
- └── src/test/java/
- └── com/
- └── example/
- └── EMS/
- ├── EmsApplicationTests.java ← Basic context load test
- ├── controller/ ← Controller tests
- │ - └── EmployeeControllerTest.java
- ├── repository/ ← Repository tests
- │ - └── EmployeeRepositoryTest.java
- └── service/ ← Service tests
- └── EmployeeServiceTest.java

- Other Important Folders/Files :

- employee-management-system/
- ├──src/main/resources/
- │ - └── application.properties
- │ - └── application-test.properties
- └──target/ ← Compiled classes, JAR file, etc

Main class

=> EMSApplication.java

Endpoints 

  1. /addEmployee
  2. /addEmployees
  3. /{id}
  4. /getEmployeeById
  5. /getAllEmployees
  6. /searchEmployeeById
  7. /searchEmployees
  8. /updateEmployeeById/{id}
  9. /updateEmployeeByName/{name}
  10. /deleteEmployeeById/{id}
  11. /deleteAllEmployees 
  12. /getAllEmployeesWithPagination
  13. /searchEmployeesWithPagination
  14. /searchEmployeesWithPagination1

@PostMapping("/addEmployee") //http://localhost:8080/api/employees/addEmployee
    public ResponseEntity<EmployeeResponseDto> addEmployee(@Valid @RequestBody EmployeeRequestDto employeeRequestDto)

=> It is used to add one Employee at a time
=> ModelMapper did not work, So used manual mapping. Manual mapping is more reliable than ModelMapper
=> It uses the default method save(employee) (from JPARepository/CRUD repository)
=> Never forget to use @Transactional in service side as it needs commit on success, rollback on error
=> url : http://localhost:8080/api/employees/addEmployee
=> Since its PostMapping and adding new entry, HttpStatus should be 201 Created
=> It is recommended to show location as well in the ResponseEntity 

@PostMapping("/addEmployees") //http://localhost:8080/api/employees/addEmployees
    public ResponseEntity<List<EmployeeResponseDto>> addEmployees (@RequestBody List<EmployeeRequestDto> employeeRequestDtoList)

=> It is used to add multiple Employees at a time
=> ModelMapper did not work, So used manual mapping. Manual mapping is more reliable than ModelMapper
=> It uses the default method saveAll(employeeList) (from JPARepository/CRUD repository)
=> Never forget to use @Transactional in service side as it needs commit on success, rollback on error, batch insert, all or nothing
=> url : http://localhost:8080/api/employees/addEmployees
=> Since its PostMapping and adding new entry, HttpStatus should be 201 Created
=> It is recommended to show location as well in the ResponseEntity

@GetMapping("/{id}") //http://localhost:8080/api/employees/{id}
    EmployeeResponseDto getEmployeeById(@PathVariable Long id)

=> It uses PathVariable
=> sample url : http://localhost:8080/api/employees/{id} 
=> It uses the default method findById(id) 
(from JPARepository/CRUD repository)

@GetMapping
    public List<EmployeeResponseDto> getAllEmployees()

=> sample url : http://localhost:8080/api/employees
=> It uses the default method findAll() (from JPARepository/CRUD repository)

@GetMapping("/searchEmployeeById") //http://localhost:8080/api/employees/searchEmployeeById?id={id}&name={name}
    EmployeeResponseDto searchEmployeeById(@Valid @RequestParam("id") Long id)
  

=> Sample url : http://localhost:8080/api/employees/searchEmployeeById?id=1
=> 
It uses RequestParam
=> It uses the default method findById(id) (from JPARepository/CRUD repository)

@GetMapping("/searchEmployees")
    public ResponseEntity<List<EmployeeResponseDto>> searchEmployees(@RequestParam(name="name", required = false) String employeeName,
                                              @RequestParam(name="department", required = false) String department,
                                              @RequestParam(name="minSalary", required = false) Integer minSalary)

=> Sample url :
        //http://localhost:8080/api/employees/searchEmployees?name={name}
        //http://localhost:8080/api/employees/searchEmployees?department={department}
        //http://localhost:8080/api/employees/searchEmployees?minSalary={minSalary}
        //http://localhost:8080/api/employees/searchEmployees?department={department}&minSalary={minSalary}
        //http://localhost:8080/api/employees/searchEmployees
=> 
It uses RequestParam
=> It uses the default method findAll() (from JPARepository/CRUD repository), then applies required filters

@PutMapping("/updateEmployeeById/{id}")
    public ResponseEntity<EmployeeResponseDto> updateEmployeeById(@Valid @RequestBody EmployeeRequestDto employeeRequestDto, @PathVariable Long id)

=> Sample url : http://localhost:8080/api/employees/updateEmployeeById/10
=> It uses PathVariable
=> It uses the default method findById(id), save(employee)
=> Since its PutMapping and updating the existing entry, HttpStatus should be 200 Ok 
=> Remember we need to throw ResourceNotFoundException if the given id does not exist
=> Since it is updating based on id (ie id is primary key, unique), It will update exactly one entry at a time

@PutMapping("/updateEmployeeByName/{name}")
    public ResponseEntity<List<EmployeeResponseDto>> updateEmployeeByName(@Valid @RequestBody EmployeeRequestDto employeeRequestDto, @PathVariable ("name") String name)

=>Sample url : http://localhost:8080/api/employees/updateEmployeeByName/Sachin 
Since it is updating based on name (ie name is not primary key, it can be duplicate), It may update multiple entries at a time
=>  
Remember we need to throw ResourceNotFoundException if no record found for the given name
=> It uses the default method findByName(name)
=> For save, it uses save(employee) in loop
=> Since its PutMapping and updating the existing entry, HttpStatus should be 200 Ok 

@DeleteMapping("/deleteEmployeeById/{id}")
    public ResponseEntity<HttpStatus> deleteEmployeeById(@PathVariable("id") Long id)

=> Sample url : http://localhost:8080/api/employees/deleteEmployeeById/4
=> Since it is deleting based on id (ie id is primary key, unique), It will delete exactly one entry at a time
=> Remember we need to throw ResourceNotFoundException if the given id does not exist
=> Since its DeleteMapping, HttpStatus should be 204 - No content

@DeleteMapping("/deleteAllEmployees")
    public ResponseEntity<HttpStatus> delete
AllEmployees()

=> url : http://localhost:8080/api/employees/deleteAllEmployees
=> It will delete all the employee records in the repository
=> Remember even if deleted all the records, on new insert the Id will be continued from where they left. Example if 10 records inserted, deleted all, then again inserting, those Id will start from 11, 12, ..
=> Since its DeleteMapping, HttpStatus should be 204 - No content

 @GetMapping("/getAllEmployeesWithPagination")
    public Page<EmployeeResponseDto> getAllEmployeesWithPagination(@PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.ASC) Pageable pageable)

=> Instead of List<EmployeeResponseDto>, it returns Page<EmployeeResponseDto> wise
=> Sample url : 
        http://localhost:8080/api/employees/getAllEmployeesWithPagination?page=0&size=5 
        http://localhost:8080/api/employees/getAllEmployeesWithPagination?page=1&size=5
        http://localhost:8080/api/employees/getAllEmployeesWithPagination?sort=salary,desc&size=10
        http://localhost:8080/api/employees/getAllEmployeesWithPagination?page=0&size=5&sort=salary,desc
=> If we do not mention query params in url, it will consider @PageableDefault values

@GetMapping("/searchEmployeesWithPagination")
    public Page<EmployeeResponseDto> searchEmployeesWithPagination(@RequestParam(name="name", required = false) String employeeName,
                                                                   @RequestParam(name="department", required=false) String department,
                                                                   @RequestParam(name="minSalary", required=false) Integer minSalary,
                                                                   Pageable pageable)

=> Sample url : http://localhost:8080/api/employees/searchEmployeesWithPagination1?page=0&size=5&name=Sach
=> To enable pagination with search filters, we can use functional interface Specification
=> The below line is depricated,  but still we added for core understanding
Specification<Employee> spec = Specification.where(null);
=> Actually, above line creates WHERE 1=1 SQL structure and so we can add chain conditions (AND/OR)
=> Remeber RQCB - Root, Query, CriteriaBuilder
=> We use employeeRepository.findAll(spec, pageable);
=> Note : We have to declare the method findAll(spec, pageable) in EmployeeRepository.java file
=> If we don't use @PageableDefault, page =0, size=20 by default      

@GetMapping("/searchEmployeesWithPagination1")
    public Page<EmployeeResponseDto> searchEmployeesWithPagination1(@RequestParam(name="name", required = false) String employeeName,
                                                                   @RequestParam(name="department", required=false) String department,
                                                                   @RequestParam(name="minSalary", required=false) Integer minSalary,
                                                                   Pageable pageable)

=> This serves the same purpose of searchEmployeesWithPagination1
=> Since the below line is depricated, we used modern ways speciation.allOf(specs), Specification.anyOf(specs)
Specification<Employee> spec = Specification.where(null);
=> Specification.allOf(specs) - Combines the filters as AND conditions in SQL
=> Specification.anyOf(specs) - Combines the filters as OR conditions in SQL

Global Exception Handler - handled exceptions List

  1. MethodArgumentNotValidException
  2. MethodArgumentTypeMismatchException
  3. BindException
  4. HandlerMethodValidationException
  5. ConstraintViolationException
  6. ResourceNotFoundException (Custom exception)
  7. DuplicateEmailException (Custom exception)
  8. Exception
 
Endpoint / Parameter TypeAnnotation UsedException Thrown on Validation FailureHandler to Catch ItTypical HTTP Status
Single DTO: @RequestBody EmployeeRequestDto dto@ValidMethodArgumentNotValidException@ExceptionHandler(MethodArgumentNotValidException.class)400 Bad Request
List DTO: @RequestBody List<EmployeeRequestDto> dtoList@ValidBindException (or wrapped MethodArgumentNotValidException)@ExceptionHandler(BindException.class) (and sometimes MethodArgumentNotValidException)400 Bad Request

 
 

JPA repository methods used

  1. findByName
  2. existsByEmail
  3. findAll
  4. findEmailsIn (@Query) 

DB table list (Entity)

  1. Employee (@Table(name="employees")) 

Actuator endpoints                 

=> DataBaseHealthIndicator (Custom health indicator)

How to enable actuator endpoints (health, info, metrics) ?

=> Ensure actuator dependency present in the pom.xml 

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

=> Add configuration in application.properties file 

# Enable Actuator endpoints (expose them over HTTP)
management.endpoints.web.exposure.include=health,info,metrics

# Show full details for health checks (e.g., database status)
management.endpoint.health.show-details=always

# Optional: Expose more if needed later (e.g., loggers)
# management.endpoints.web.exposure.include=health,info,metrics,loggers

# Optional: Change Actuator base path (default is /actuator)
# management.endpoints.web.base-path=/manage

NOTE : management.endpoint.health.show-details=always → shows component details (DB, disk, etc.) 

=> Restart the app 

=> Verify the endpoints 

http://localhost:8080/actuator/health
http://localhost:8080/actuator/info
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/http.server.requests

Custom Health Indicator 

=> Created DataBaseHealthIndicator (this is custom health indicator)
=> Without custom health indicator, it will just show whether the database is up or not, no extra details 

 

=> With custom health indicator, it will show extra information that we added in the overridden health() method 

 

How to make H2 database down for testing purpose ?

=> Create src/main/resources/application-test.properties
# Invalid URL to simulate DB down
spring.datasource.url=jdbc:h2:mem:invalid-db-name;DB_CLOSE_DELAY=-1;INVALID_PARAM=true 

=> IntelliJ → Edit Configurations → add to Program arguments:   

--spring.profiles.active=test 

 => Restart the app, the app will not be running up. It will throw some exceptions in the console 

=> So, the only way to test the /health to show db down is : 

Modify DatabaseHealthIndicator to force DOWN in test profile (temporary for testing)

package com.example.employeeManagementSystem.config;
import org.springframework.core.env.Environment;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Arrays;

@Component
public class DataBaseHealthIndicator implements HealthIndicator {

    private final DataSource dataSource;
    private final Environment environment;

    //Constructor Injection
    public DataBaseHealthIndicator(DataSource dataSource, Environment environment){
        this.dataSource = dataSource;
        this.environment = environment;
    }

    @Override //http://localhost:8080/actuator/health
    public Health health(){

        boolean isTestProfile = Arrays.asList(environment.getActiveProfiles()).contains("test");

        if (isTestProfile) {
            // Simulated DOWN for test profile
            return Health.down()
                    .withDetail("profile", "test")
                    .withDetail("status", "Simulated DOWN for testing")
                    .build();
        }

        // Normal DB check for all other profiles
        try(Connection connection = dataSource.getConnection()){
            if(connection.isValid(1)){ // 1 second timeout
                DatabaseMetaData dataBaseMetaData = connection.getMetaData();

                return Health.up()
                        .withDetail("Database", "Connected Successfully")
                        .withDetail("DataBaseName", dataBaseMetaData.getDatabaseProductName())
                        .withDetail("DataBaseVersion", dataBaseMetaData.getDatabaseProductVersion())
                        .withDetail("Driver", dataBaseMetaData.getDriverName())
                        .withDetail("DriverVersion", dataBaseMetaData.getDriverVersion())
                        .build();
            }else{
                return Health.down()
                        .withDetail("Reason","Connection is not valid")
                        .build();
            }

        }catch (SQLException e){
            return Health.down()
                    .withDetail("error", "Failed to connect "+e.getMessage())
                    .withException(e)
                    .build();
        }
    }

}

=> Run the test profile 

=> Alternate way is to have two separate classes 

1. TestHealthIndicator (only for test profile)

@Component
@Profile("test")
public class TestHealthIndicator implements HealthIndicator {
    @Override
    public Health health() {
        return Health.down()
                .withDetail("profile", "test")
                .withDetail("status", "Simulated DOWN for testing")
                .build();
    }
}

2. DatabaseHealthIndicator (for non-test)

@Component
@Profile("!test")
public class DatabaseHealthIndicator implements HealthIndicator {
    // Your normal DB check code...
}

=> Spring registers only one at runtime based on active profile, no name conflict 

TEST CASES - @WebMvcTest + MockMVC

=> @ExtendWith(MockitoExtension.class) + @Mock / @InjectMocks is the pure Mockito way (no Spring at all).

=> @WebMvcTest is the official, recommended way for testing Spring MVC controllers in Spring Boot

=> So, prefer practising @WebMvcTest way of test cases 

=>  @WebMvcTest + MockMvc - for Controller test

=> @MockitoBean - for Service test (@MockBean is deprecated)

=> @DataJpaTest - for Repository test 

=> @SpringBootTest - End to end integration test with full spring context i.e full flow test

=> Example : 

@Test //Success - Valid request → 201 Created with Location header & body
    void testAddEmployee_Success() throws Exception{

        //Prepare request DTO
        EmployeeRequestDto employeeRequestDto = new EmployeeRequestDto("Sachin", 900000, "Cricket", "sachin@gmail.com");

        //Prepare expected response DTO
        EmployeeResponseDto employeeResponseDto = new EmployeeResponseDto(1L, "Sachin", 900000, "Cricket", "sachin@gmail.com");

        //Mock service behavior
        when(employeeService.addEmployee(any(EmployeeRequestDto.class))).thenReturn(employeeResponseDto);

        //Perform POST request
        mockMvc.perform(post("/api/employees/addEmployee")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(employeeRequestDto)) //Converts Java DTO to JSON string for request body
                )
                .andExpect(status().isCreated()) //201 created
                .andExpect(header().string("location", containsString("api/employees/1")))
                .andExpect(jsonPath("$.id").value(1L))
                .andExpect(jsonPath("$.name").value("Sachin"))
                .andExpect(jsonPath("$.salary").value(900000))
                .andExpect(jsonPath("$.department").value("Cricket"))
                .andExpect(jsonPath("$.email").value("sachin@gmail.com"));

        //Verify service was called once
        verify(employeeService, times(1)).addEmployee(any(EmployeeRequestDto.class));
    }

Explanation of key parts

@WebMvcTest(EmployeeController.class)

=> Loads only the web layer (controller, filters, message converters)
=> No full context, no real service/DB — very fast

@MockitoBean EmployeeService 

=> Replaces the real EmployeeService with a mock in the Spring context
=> You control what it returns or throws

MockMvc

=> Simulates HTTP requests (POST with JSON body)
=> perform(post(...)) sends the request
=> andExpect(...) asserts response (status, JSON content, headers)

objectMapper.writeValueAsString()

=> Converts Java DTO to JSON string for request body 

jsonPath(...)

=> Asserts specific JSON fields in response (e.g., $.name)

verify(...)

=> Ensures service method was called exactly once (or never on validation fail)

Ensure the dependencies needed (in pom.xml) : 

=> spring-boot-starter-test (includes JUnit 5, Mockito, MockMvc, AssertJ, Jackson)

Key points to remember 

=> One endpoint API can have the following 3 test cases (referred the example endpoint)

  1. Success case (valid request → 201 Created + Location header + response body)
  2. Validation failure (invalid DTO → 400 Bad Request + error messages)
  3. Service exception (e.g., any business error → 500 or custom status)

=> Test case consists of the following 5 parts (often, not always)

  1. Prepare request DTO
  2. Prepare expected response DTO
  3. Mock service behavior (when..thenReturn or when..thenThrow)
  4. MockMvc perform http request (mockMvc.perform(post())..andExpect(...);)
  5. Verify the service was called exactly once or never depending upon the case (Eg. verify(employeeService, times(1)).addEmployee(any(EmployeeRequestDto.class));)

--------------------------------------------------------------------------------------------------------

Interview Readiness - preparation plan

DayFocus AreaDurationWhat We Will Do
1Project Overview + Storytelling1 hourCreate strong 2-3 minute introduction + overall architecture explanation
2Technical Deep Dive (Core)1–1.5 hrsDatabase design, API design, Layered architecture, DTOs, Exception handling
3Challenges, Problem Solving & Decisions1–1.5 hrsMost important for senior roles – challenges you faced + how you solved them
4Advanced Topics + Improvements1 hourScalability, Security, Performance, Future improvements, Monitoring
5Full Mock Interview + Revision1–1.5 hrsComplete simulated interview + feedback + weak area revision

Project employee-management-system introduction

=> The Employee Management System (EMS) is a complete RESTful backend API, I built using Spring Boot to manage employee data efficiently
=> It supports full CRUD operations, batch employee creation, advanced search and filtering by name, department, and salary, along with pagination and sorting
=> I followed a clean layered architecture – Controller, Service, Repository pattern with proper separation of concerns
=> I used DTOs to avoid exposing internal entities to the client
=> Implemented bean validation, custom exceptions with global exception handling, and added Spring Boot Actuator for basic monitoring   
=> The project is built with Java 25, Spring Boot 3.5.9, Spring Data JPA, and H2 in-memory database. I also wrote unit and integration tests using JUnit 5 and Mockito.
=> The main goal of this project was to demonstrate modern Spring Boot best practices, clean code principles, and production-ready features  

High level architecture explanation

=> I designed the application using the Layered Architecture pattern, which is very common in Spring Boot applications
=> Controller Layer: Handles all incoming HTTP requests, validates input using @Valid, and delegates work to the Service layer. It returns proper HTTP status codes and ResponseEntity.
=> Service Layer: Contains all business logic. I used an interface + implementation approach (EmployeeService & EmployeeServiceImpl). Here I do DTO ↔ Entity mapping using ModelMapper.
=> Repository Layer: Extends JpaRepository which gives me all CRUD operations out of the box. I also added custom query methods using Spring Data JPA Specifications for dynamic search with filters. 
Additionally, I have:
=> DTO Layer (EmployeeRequestDto, EmployeeResponseDto) for request/response separation.
=> Exception Handling: Global exception handler using @ControllerAdvice.
=> Config Layer: For exception handling, health indicators, etc. 
=> This architecture gives good separation of concerns, easy testability, and maintainability.  
 
Tech stack justification
 
=> I chose Spring Boot 3.5.9 because it provides excellent developer productivity with auto-configuration and production-ready features. 
=> Java 17 gives me modern language features like records, sealed classes, and better performance. Considering future exension, I chose Java 25
=> Spring Data JPA with H2 was perfect for this project as it allows rapid development and easy demonstration of ORM concepts.
=> In real production projects, I have used PostgreSQL / MySQL with the same pattern.
=> For monitoring, I integrated Spring Boot Actuator because it provides health checks and application metrics out of the box — meaning I got them automatically without writing extra code. 
("Out of the box" means → "Ready to use immediately without any extra effort or additional configuration.") 
 
Database Design 
 
=> I used H2 in-memory database with Spring Data JPA for this project
=> The main entity is Employee with fields: id, name, salary, department, and email
=> I used standard JPA annotations like @Entity, @Table(name = "employees"), and @Id with GenerationType.IDENTITY 
=> I did not use @Column on individual fields because the Java field names match the required column names, and JPA maps them by default
=> In a real production project, I would switch to PostgreSQL and add @Column for constraints like nullable=false and unique=true on email
 
Why H2 ? 
 
=> For quick development and demo purposes 
=> In a real production environment, I would replace H2 with PostgreSQL and add proper indexing on frequently searched columns like department and salary. 
 
API design and endpoints

All APIs are under the base path /api/employees. I implemented the following key endpoints:

  1. POST /api/employees/addEmployee → Create single employee
  2. POST /api/employees/addEmployees → Create multiple employees (Batch)
  3. GET /api/employees → Get all employees
  4. GET /api/employees/{id} → Get employee by ID (Path Variable)
  5. GET /api/employees/getEmployeeById → Get employee by ID (RequestParam)
  6. GET /api/employees/searchEmployees → Search employees by name, department, minSalary
  7. GET /api/employees/getAllEmployeesWithPagination → Get all employees with pagination & sorting
  8. GET /api/employees/searchEmployeesWithPagination → Search with pagination
  9. PUT /api/employees/updateEmployeeById/{id} → Update employee by ID
  10. PUT /api/employees/updateEmployeeByName/{name} → Update employee(s) by name
  11. DELETE /api/employees/deleteEmployeeById/{id} → Delete by ID
  12. DELETE /api/employees/deleteAllEmployees → Delete all employees

I used @RequestBody, @PathVariable, @RequestParam, and Pageable for pagination. All responses are wrapped in ResponseEntity with proper HTTP status codes (201 for create, 200 for success, 204 for delete, etc.)

Major Challenges & How You Solved Them
 
Challenge 1: ModelMapper Configuration & Usage
 
=> I used ModelMapper for converting between EmployeeRequestDto, EmployeeResponseDto, and the Employee entity
=> I created a dedicated ModelMapperConfig class to configure the bean. Using ModelMapper helped reduce boilerplate code for object mapping and kept the service layer cleaner.
 
Challenge 2: Implementing Dynamic Search with Multiple Filters
 
=> I used Spring Data JPA Specification for dynamic filters
=> I tried deprecated way (Specification<Employee> specification = Specification.where(null);) for better understanding
=> And also I tried the Specification.allOf method way. 
=> Im aware of both the Specification.allOf and Specification.anyOf
 
Challenge 3: Global Exception Handling
 
=> Making sure all errors (validation, resource not found, duplicate email, etc.) return consistent and meaningful responses was important
=> I implemented a GlobalExceptionHandler using @ControllerAdvice and handled multiple exception types. This made the API more robust and professional
=> That helped me to understand when and where the exceptions like MethodArgumentNotValidException, ConstraintViolationException are thrown
 
Challenge 4: Batch Operations with Proper Transaction Management
 
=> For the batch insert endpoint (addEmployees), I needed to ensure atomicity — either all records are saved or none
=> I used @Transactional on the service method along with saveAll() from JpaRepository. This handled rollback automatically in case of any failure (like duplicate email)
 
Challenge 5: Supporting Multiple API Variations
 
=> I implemented both PathVariable and RequestParam versions for some operations, along with basic and paginated versions of search
=> This was done to demonstrate different ways of handling similar requirements in Spring Boot