MEDICARE HEALTHCARE MANAGEMENT SYSTEM

Business Scenario

In this lab, we will explore the architecture and development approach for the Medicare Healthcare Management System, a full-stack app using React, Spring Boot, and MySQL.

Project Introduction & Development Guidelines

What is the Project All About?

1

The Medicare Healthcare Management System is a full-stack web application designed to simplify the process of finding doctors, booking appointments, making consultation payments, managing doctor schedules, and handling digital prescriptions.

The project provides separate functionality for two major users:

  • Patient

  • Doctor

A patient can register, log in, search for doctors, apply filters, book an appointment, make a payment, track appointment status, and view prescriptions after the consultation.

A doctor can log in, manage their professional profile, view scheduled appointments, complete consultations, and create prescriptions for patients.

Why Did We Choose This Project?

2

Healthcare appointment systems are commonly used in hospitals, clinics, telemedicine applications, and healthcare platforms.

We selected this project because it allows us to understand several important full-stack development concepts within a single application.

Through this project, students will learn how different technologies communicate with each other in a real application.

The project covers:

  • Online payment integration

  • Email notifications

  • Prescription management

  • REST API development

  • Frontend-backend integration

  • Exception handling

  • Form validation

  • Security and ownership validation

  • User registration and login

  • Authentication using JWT

  • Role-based authorization

  • Database relationships

  • Doctor profile management

  • Searching and filtering data

  • Pagination and sorting

  • Appointment booking

What Are We Going to Create?

3

  • We are going to build a complete Healthcare Appointment Management System.
  • The system will mainly contain the following modules.

Patient Module

  • Register into the system

  • Login using email and password

  • View their dashboard

  • View their profile

  • Search for doctors

  • Search doctors by name

  • Filter doctors by specialization

  • Select a doctor

  • Select appointment date

  • Select available time slot

  • Book an appointment

  • Make payment using Razorpay Test Mode

  • View appointment history

  • Cancel an unpaid appointment

Patient Module


  • Filter doctors by consultation fee

  • Sort doctors

  • View doctors using pagination

  • View appointment status

  • View payment status

  • View prescription after consultation

  • Receive appointment confirmation email

Patient Module

  • Login into the system

  • View doctor dashboard

  • Create and manage doctor profile

  • Add specialization

  • Add years of experience

  • Set consultation fees

  • ​​View payment status

  • Complete confirmed appointments

  • Create prescription

  • Add diagnosis

  • Add multiple medicines

  • Add dosage

Doctor Module


  • Add profile photo URL
  • View scheduled patient appointments

  • View patient information

  • View appointment date and time

  • Add frequency

  • Add duration

  • Add doctor notes

Technologies Used

4

The project uses the following technologies:

  • Frontend

React, Vite, JavaScript, Bootstrap, Axios, React Router, Context API


  • Backend

Java, Spring Boot, Spring Web, Spring Data JPA, Hibernate, Spring Security, JWT Authentication, Bean Validation

  • Database

MySql

  • External Integrations

Razorpay Payment Gateway, Email / SMTP Service


  • Development Tools

Eclipse / Spring Tool Suite, Visual Studio , Postma, MySQL Workbench, Git, GitHub

       USER
         │
         ▼
   React Frontend
         │
      REST API
         ▼
 Spring Boot Backend
         │
    ┌────┼────┐
    ▼    ▼    ▼
 MySQL Razorpay Email

Technologies Used

5

  • The application follows a standard frontend-backend-database architecture.
  • React is responsible for the user interface.
  • Spring Boot handles backend logic
  • MySQL stores application data permanently.

Complete Application Flow

6

  • Patient Flow
  • Doctor Flow
  • Appointment status flow
  • If the patient cancels before payment:
PENDING + UNPAID
        │
        ▼
    CANCELLED
  • A paid appointment cannot be cancelled in the current project because a refund module is outside the project scope.

ER Diagram of the Project

7

  • The ER Diagram represents how different database tables are connected with each other.
  • The main entities represented in the database include: Users, Doctor Profiles, Appointments, Prescriptions, Prescription Medicines
  • Note About the ER Diagram

The supplied ER diagram currently shows both doctors and: doctor_profiles

  • The final project architecture primarily uses the doctor_profiles table connected to the users table.

How Should We Approach This Project and how to read and use Each phase?

8

Build the foundation before starting the labs

Before beginning project implementation, the trainer should take at least 3 foundation sessions of approximately 2 hours each.

Initialize Git before starting development

  • Create one parent project folder and initialize Git inside that parent folder.

medicare-healthcare-system/
│
├── medicare-backend/
│
└── medicare-frontend/
  •  The structure should be: 
  • Create one parent project folder and initialize Git inside that parent folder.

  • Both frontend and backend should therefore be maintained inside the same parent Git repository.

Read the phase objective first

  • Before writing or copying any code, understand what functionality is being developed in that phase.

  • Identify what the application should be able to do after completing that phase.

Check the files involved in the phase

  • Identify: Files that need to be created, Files that need to be modified, Existing files that will be reused

  • Always paste or write code in the exact file and folder mentioned in the lab

Understand the flow before implementing

  • Understand how the feature moves through the application.
  • For example:
React -> Controllerc->Servicec-> Repository -> MySQL

Follow the phases in sequence

  • Each phase is built on top of the previous phase.

  • Do not randomly skip phases unless specifically instructed.

  • Keep previously completed functionality working while adding the next feature.

Focus on understanding rather than manually typing everything

  • These labs are unintentionally large, so students are not expected to manually type every line of code.

  • Students are encouraged to copy suitable code provided in the labs to save implementation time.

  • However, copied code must be understood before it is used.

Do not blindly copy and paste

  • Before copying code, understand: What the code is doing, Why it is required, Which file it belongs to, How it connects with existing code, What input and output it expects

  • DoctorProfile itself does not contain: name because name belongs to: User

  • So Spring Data can navigate the relationship:
List<DoctorProfile> findByUser_NameContainingIgnoreCase(String name);
  • Read that as:

DoctorProfile -> User -> Name -> Containing -> IgnoreCase

  • For example Search: anaya can match
Dr. Ananya Sharma
  • And:
ANANYA
ananya
AnAnYa

all work because of: IgnoreCase

But there is a problem...

  • Suppose we create methods for every possible combination:
specialization
name
maxFee
specialization + name
specialization + maxFee
name + maxFee
specialization + name + maxFee
  • We'd eventually end up with ridiculous method names such as:
findBySpecializationIgnoreCaseAndConsultationFeeLessThanEqualAndUser_NameContainingIgnoreCase(...)
  • Technically possible, but not clean

Add a Doctor Response DTO

4

  • This is where we will introduce another DTO.
  • Why? :  Currently returning DoctorProfile gives something like:
{
    "id": 1,
    "user": {
        "id": 2,
        "name": "Dr. Ananya Sharma",
        "email": "ananya@example.com",
        "phone": "9876543211",
        "role": "DOCTOR"
    },
    "specialization": "Cardiologist",
    "experienceYears": 8,
    "consultationFee": 1200
}
  • The patient doctor listing doesn't need:
email
phone
role
  • It needs:
Doctor ID
Doctor Name
Specialization
Experience
Fee
Photo
  • So this is a genuine reason for a DTO

Create DoctorResponse

5

dto/
└── DoctorResponse.java
package com.medicare.dto;
import java.math.BigDecimal;

public class DoctorResponse {

    private Long id;
    private String name;
    private String specialization;
    private Integer experienceYears;
    private BigDecimal consultationFee;
    private String photoUrl;
  • Create: 

    public DoctorResponse(
            Long id,
            String name,
            String specialization,
            Integer experienceYears,
            BigDecimal consultationFee,
            String photoUrl) {

        this.id = id;
        this.name = name;
        this.specialization = specialization;
        this.experienceYears = experienceYears;
        this.consultationFee = consultationFee;
        this.photoUrl = photoUrl;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getSpecialization() {
        return specialization;
    }

    public Integer getExperienceYears() {
        return experienceYears;
    }

    public BigDecimal getConsultationFee() {
        return consultationFee;
    }

    public String getPhotoUrl() {
        return photoUrl;
    }
}

Update DoctorProfileRepository

6

  • Instead of allowing users to type any random value such as:

repository/
└── DoctorProfileRepository.java
  • Create: 

  • Our final repository can be:
package com.medicare.repository;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.medicare.entity.DoctorProfile;

public interface DoctorProfileRepository
        extends JpaRepository<DoctorProfile, Long> {

    boolean existsByUserId(Long userId);

    @Query(""" 
            SELECT d FROM DoctorProfile d JOIN d.user u WHERE
                (:specialization IS NULL 
                   OR LOWER(d.specialization) = LOWER(:specialization))
            AND
                (:maxFee IS NULL OR d.consultationFee <= :maxFee)
            AND
                (:name IS NULL 
                  OR LOWER(u.name) LIKE CONCAT(CONCAT('%', LOWER(:name)), '%'))
            """)
    List<DoctorProfile> searchDoctors(
            @Param("specialization")
            String specialization,

            @Param("maxFee")
            BigDecimal maxFee,

            @Param("name")
            String name
    );
}

Understand JPQL

7

  • We wrote : SELECT d FROM DoctorProfile d
  • Notice: DoctorProfile  and not: doctor_profiles

  • Why? - Because this is: JPQL not raw SQL.

Why JOIN d.user u?

  • Our doctor name exists inside:

DoctorProfile -> User -> Name

  • So: JOIN d.user u gives the query access to u.name
  • Then we can search: LOWER(u.name)

Optional Specialization

  • This portion:
(:specialization IS NULL
    OR LOWER(d.specialization) = LOWER(:specialization))
  • means
Did user provide specialization?
       │
   ┌───┴───┐
   NO      YES
   │        │
 ignore   match specialization
 filter

Maximum Fee

  • This portion:
(:maxFee IS NULL OR d.consultationFee <= :maxFee)
  • means if: maxFee = 1000 return doctors charging:

₹300
₹500
₹700
₹900
₹1000
  • but not:
₹1200
₹1500
₹2000

Doctor Name Search

  • This portion means: contains
LOWER(u.name)
LIKE CONCAT(CONCAT('%', LOWER(:name)), '%')
  • So searching: ananya can find: Dr. Ananya Sharma
  • Searching: sharma can also find: Dr. Ananya Sharma

Update DoctorService

8

  • We're also going to start returning DoctorResponse instead of exposing the complete DoctorProfile entity through our public doctor APIs.
package com.medicare.service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import com.medicare.dto.DoctorProfileRequest;
import com.medicare.dto.DoctorResponse;
import com.medicare.entity.DoctorProfile;
import com.medicare.entity.Role;
import com.medicare.entity.User;
import com.medicare.exception.DoctorNotFoundException;
import com.medicare.exception.UserNotFoundException;
import com.medicare.repository.DoctorProfileRepository;
import com.medicare.repository.UserRepository;

@Service
public class DoctorService {

    private final DoctorProfileRepository doctorRepository;
    private final UserRepository userRepository;

    public DoctorService(
            DoctorProfileRepository doctorRepository,
            UserRepository userRepository) {
        this.doctorRepository = doctorRepository;
        this.userRepository = userRepository;
    }

    public DoctorResponse createDoctor(DoctorProfileRequest request) {

        User user = findUserById( request.getUserId() );

        if (user.getRole() != Role.DOCTOR) {

            throw new IllegalArgumentException(
                    "Doctor profile can only be created for a DOCTOR user"
            );
        }

        if (doctorRepository.existsByUserId(user.getId())) {

            throw new IllegalArgumentException(
                    "Doctor profile already exists for this user"
            );
        }

        DoctorProfile doctor = new DoctorProfile();

        doctor.setUser(user);

        doctor.setSpecialization(
                request.getSpecialization()
        );
        doctor.setExperienceYears(request.getExperienceYears());
        doctor.setConsultationFee(request.getConsultationFee());
        doctor.setPhotoUrl( request.getPhotoUrl());
        DoctorProfile savedDoctor = doctorRepository.save(doctor);
        return mapToDoctorResponse(savedDoctor);
    }

    public List<DoctorResponse> searchDoctors(
          String specialization, BigDecimal maxFee, String name) {
        specialization = normalizeText(specialization);
        
        name = normalizeText(name);

        if (maxFee != null && maxFee.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException(
                    "Maximum consultation fee must be greater than zero"
            );
        }

        List<DoctorProfile> doctors = doctorRepository.searchDoctors(
           specialization, maxFee, name);

        List<DoctorResponse> responses = new ArrayList<>();

        for (DoctorProfile doctor : doctors) {
            responses.add(mapToDoctorResponse(doctor));
        }
        return responses;
    }
    public DoctorResponse getDoctorById(Long id) {
        DoctorProfile doctor = findDoctorById(id);
        return mapToDoctorResponse(doctor);
    }

    public DoctorResponse updateDoctor(Long id, DoctorProfileRequest request) {

        DoctorProfile doctor = findDoctorById(id);
        doctor.setSpecialization(request.getSpecialization());
        doctor.setExperienceYears(request.getExperienceYears());
        doctor.setConsultationFee(request.getConsultationFee());
        doctor.setPhotoUrl(request.getPhotoUrl());
        DoctorProfile updatedDoctor =doctorRepository.save(doctor);
        return mapToDoctorResponse(updatedDoctor);
    }

    public void deleteDoctor(Long id) {
        DoctorProfile doctor = findDoctorById(id);
        doctorRepository.delete(doctor);
    }

    private DoctorProfile findDoctorById(Long id) {

        return doctorRepository.findById(id).orElseThrow(() ->
                        new DoctorNotFoundException("Doctor not found with id: "
                          + id));
    }

    private User findUserById(Long id) {
        return userRepository
                .findById(id)
                .orElseThrow(() ->
                        new UserNotFoundException(
                                "User not found with id: " + id
                        )
                );
    }

    /*
     * Converts DoctorProfile entity into the simpler
     * response sent to the frontend.
     */
    private DoctorResponse mapToDoctorResponse(
            DoctorProfile doctor) {

        return new DoctorResponse(
                doctor.getId(),
                doctor.getUser().getName(),
                doctor.getSpecialization(),
                doctor.getExperienceYears(),
                doctor.getConsultationFee(),
                doctor.getPhotoUrl()
        );
    }

    /*
     * Empty request parameters should behave
     * the same as parameters that were not supplied.
     */
    private String normalizeText(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        return value.trim();
    }
}

Why normalizeText()?

  • Suppose React accidentally sends: GET /api/doctors?name=
  • Technically: name = "" rather than: null
  • We convert:
""
" "
"     "
  • to:
null

so those empty filters don't affect the query.

Update DoctorController

9

  • Our previous:
@GetMapping
public ResponseEntity<List<DoctorProfile>>
getAllDoctors()

will now become dynamic.

package com.medicare.controller;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.medicare.dto.DoctorProfileRequest;
import com.medicare.dto.DoctorResponse;
import com.medicare.service.DoctorService;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/doctors")
public class DoctorController {

    private final DoctorService doctorService;

    public DoctorController(DoctorService doctorService) {
        this.doctorService = doctorService;
    }

    @PostMapping
    public ResponseEntity<DoctorResponse>
            createDoctor(@Valid @RequestBodyDoctorProfileRequest request) {
        DoctorResponse savedDoctor = doctorService.createDoctor(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedDoctor);
    }

    @GetMapping
    public ResponseEntity<List<DoctorResponse>>
            searchDoctors(
                    @RequestParam(required = false)
                    String specialization,

                    @RequestParam(required = false)
                    BigDecimal maxFee,

                    @RequestParam(required = false)
                    String name) {
        List<DoctorResponse> doctors =
                doctorService.searchDoctors(specialization,maxFee,name);
        return ResponseEntity.ok(doctors);
    }

    @GetMapping("/{id}")
    public ResponseEntity<DoctorResponse>
            getDoctorById(@PathVariable Long id) {
        return ResponseEntity.ok(
                doctorService.getDoctorById(id)
        );
    }

    @PutMapping("/{id}")
    public ResponseEntity<DoctorResponse>
            updateDoctor(
                    @PathVariable Long id,
                    @Valid
                    @RequestBody
                    DoctorProfileRequest request) {

        return ResponseEntity.ok(doctorService.updateDoctor(id,request)
        );
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteDoctor(@PathVariable Long id) {

        doctorService.deleteDoctor(id);

        return ResponseEntity.noContent().build();
    }
}
  • We've kept: GET /api/doctors as the main doctor-list endpoint.
/api/doctors/all
/api/doctors/filter
/api/doctors/search
/api/doctors/by-name
/api/doctors/by-fee
  • We didn't create:
  • That would unnecessarily clutter the API.

Test: Get All Doctors

10

  • Our previous:
GET http://localhost:8080/api/doctors
  • Expected :

Search by Specialization

11

GET http://localhost:8080/api/doctors?specialization=Cardiologist
200 OK
  • Expected:
[
    {
        "id": 1,
        "name": "Dr. Ananya Sharma",
        "specialization": "Cardiologist",
        "experienceYears": 8,
        "consultationFee": 1200.00,
        "photoUrl": "https://example.com/ananya.jpg"
    }
]

Search by Doctor Name

12

GET
http://localhost:8080/api/doctors?name=ananya

Filter by Maximum Fee

13

GET
http://localhost:8080/api/doctors?maxFee=1000
  • Suppose:
Dr. Rohan     ₹700
Dr. Priya     ₹900
Dr. Ananya    ₹1200
Dr. Arjun     ₹1500
  • Result:
Dr. Rohan 
Dr. Priya
Dr. Ananya 
Dr. Arjun

Combine Filters

14

GET /api/doctors?specialization=Cardiologist&maxFee=1500&name=ananya
  • The result must satisfy all three conditions

Search With Only Two Filters

15

GET /api/doctors?specialization=Cardiologist&maxFee=1300
  • Filters are optional, so this also works:

Invalid Maximum Fee

16

GET /api/doctors?maxFee=-500
  • Filters are optional, so this also works:
  • Our service detects:
maxFee.compareTo(BigDecimal.ZERO) <= 0
  • and throws:
IllegalArgumentException
  • which our existing global handler converts into:
400 BAD REQUEST

What If No Doctor Matches?

17

  • Suppose
GET /api/doctors?specialization=SpaceDoctor
  • No matches.
  • Should we return: 404 NOT FOUND
  • No : We should return: 200 OK
  • Why? - Because the collection /api/doctors exists. The search simply returned zero matches.

  

We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below: 

 

Great job!

You have successfully implemented doctor search and dynamic filtering in the Medicare backend using Spring Data JPA, custom queries, and DTOs.

Checkpoint

   Git Push

git push origin branchName

Next-Lab Preparation

Module:

1) Additional Components in Project​