Content ITV PRO
This is Itvedant Content department
Business Scenario
In this lab, we will extend the Medicare backend by developing the Appointment Module, which connects patients and doctors through appointment bookings.
Pre-Lab Preparation
Phase 5 — Appointment Module + @ManyToOne / @OneToMany
Module:
1) Advance Spring data JPA and production ready Rest APi
Create Appointment Enums
1
entity/AppointmentStatus.java
package com.medicare.entity;
public enum AppointmentStatus {
PENDING,
CONFIRMED,
COMPLETED,
CANCELLED
}Create PaymentStatus
2
entity/PaymentStatus.java
package com.medicare.entity;
public enum PaymentStatus {
UNPAID,
PAID,
FAILED
}AppointmentStatus.PENDING PaymentStatus.UNPAID
Create TimeSlot
3
package com.medicare.entity;
public enum TimeSlot {"10" "Morning" "10am" "Ten AM"
entity/TimeSlot.java
MORNING_10_AM,
AFTERNOON_2_PM,
EVENING_5_PM
}10:00 AM 02:00 PM 05:00 PM
MORNING_10_AM AFTERNOON_2_PM EVENING_5_PM
Create Appointment
4
entity/Appointment.java
package com.medicare.entity;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;@Entity
@Table(name = "appointments")
public class Appointment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "patient_id", nullable = false)
private User patient;
@ManyToOne
@JoinColumn(name = "doctor_id", nullable = false)
private DoctorProfile doctor;
@Column(name = "appointment_date", nullable = false)
private LocalDate appointmentDate;
@Enumerated(EnumType.STRING)
@Column(name = "time_slot", nullable = false)
private TimeSlot timeSlot;
@Column(name = "consultation_fee", nullable = false, precision = 10, scale = 2)
private BigDecimal consultationFee;
@Enumerated(EnumType.STRING)
@Column(name = "appointment_status", nullable = false)
private AppointmentStatus appointmentStatus = AppointmentStatus.PENDING; @Enumerated(EnumType.STRING)
@Column(name = "payment_status", nullable = false)
private PaymentStatus paymentStatus = PaymentStatus.UNPAID;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt = LocalDateTime.now();
public Appointment() {
}
public Long getId() {
return id;
}
public User getPatient() {
return patient;
}
public void setPatient(User patient) {
this.patient = patient;
}
public DoctorProfile getDoctor() {
return doctor;
}
public void setDoctor(DoctorProfile doctor) {
this.doctor = doctor;
} public LocalDate getAppointmentDate() {
return appointmentDate;
}
public void setAppointmentDate(LocalDate appointmentDate) {
this.appointmentDate = appointmentDate;
}
public TimeSlot getTimeSlot() {
return timeSlot;
}
public void setTimeSlot(TimeSlot timeSlot) {
this.timeSlot = timeSlot;
}
public BigDecimal getConsultationFee() {
return consultationFee;
}
public void setConsultationFee(BigDecimal consultationFee) {
this.consultationFee = consultationFee;
}
public AppointmentStatus getAppointmentStatus() {
return appointmentStatus;
}
public void setAppointmentStatus(AppointmentStatus appointmentStatus) {
this.appointmentStatus = appointmentStatus; }
public PaymentStatus getPaymentStatus() {
return paymentStatus;
}
public void setPaymentStatus(PaymentStatus paymentStatus) {
this.paymentStatus = paymentStatus;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}Add @OneToMany to User
5
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.OneToMany;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.OneToMany;@OneToMany(mappedBy = "patient")
@JsonIgnore
private List<Appointment> appointments = new ArrayList<>();Add @OneToMany to DoctorProfile
6
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.OneToMany;@OneToMany(mappedBy = "doctor")
@JsonIgnore
private List<Appointment> appointments =
new ArrayList<>();Create Appointment Request DTO
7
{
"patient": {
"id": 1
},
"doctor": {
"id": 2
} }{
"patientId": 1,
"doctorId": 2,
"appointmentDate": "2026-09-20",
"timeSlot": "MORNING_10_AM"
}dto/AppointmentRequest.java
package com.medicare.dto;
import java.time.LocalDate;
import com.medicare.entity.TimeSlot;
import jakarta.validation.constraints.FutureOrPresent;
import jakarta.validation.constraints.NotNull;
public class AppointmentRequest {
@NotNull(message = "Patient id is required")
private Long patientId;
@NotNull(message = "Doctor id is required")
private Long doctorId;
@NotNull(message = "Appointment date is required")
@FutureOrPresent( message = "Appointment date cannot be in the past")
private LocalDate appointmentDate; @NotNull(message = "Time slot is required")
private TimeSlot timeSlot;
public Long getPatientId() {
return patientId;
}
public void setPatientId(Long patientId) {
this.patientId = patientId;
}
public Long getDoctorId() {
return doctorId;
}
public void setDoctorId(Long doctorId) {
this.doctorId = doctorId;
}
public LocalDate getAppointmentDate() {
return appointmentDate;
}
public void setAppointmentDate(
LocalDate appointmentDate) {
this.appointmentDate = appointmentDate;
}
public TimeSlot getTimeSlot() { return timeSlot;
}
public void setTimeSlot(TimeSlot timeSlot) {
this.timeSlot = timeSlot;
}
}Create Appointment Repository
8
package com.medicare.repository;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medicare.entity.Appointment;
import com.medicare.entity.TimeSlot;
repository/AppointmentRepository.java
public interface AppointmentRepository extends JpaRepository<Appointment, Long> {
List<Appointment> findByPatientId( Long patientId );
List<Appointment> findByDoctorId( Long doctorId );
boolean existsByDoctorIdAndAppointmentDateAndTimeSlot(
Long doctorId,
LocalDate appointmentDate,
TimeSlot timeSlot
);
}Create Appointment Repository
9
repository/AppointmentRepository.java
package com.medicare.repository;
import java.time.LocalDate;import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.medicare.entity.Appointment;
import com.medicare.entity.TimeSlot;
public interface AppointmentRepository
extends JpaRepository<Appointment, Long> {
List<Appointment> findByPatientId(
Long patientId
);
List<Appointment> findByDoctorId(
Long doctorId
);
boolean existsByDoctorIdAndAppointmentDateAndTimeSlot(
Long doctorId,
LocalDate appointmentDate,
TimeSlot timeSlot
);
}Create Appointment Exception
10
exception/AppointmentNotFoundException.java
package com.medicare.exception;
public class AppointmentNotFoundException
extends RuntimeException {
public AppointmentNotFoundException(
String message) {
super(message);
}
}Create Slot Conflict Exception
11
exception/AppointmentConflictException.java
package com.medicare.exception;
public class AppointmentConflictException
extends RuntimeException {
public AppointmentConflictException(
String message) {
super(message);
}
}Create Appointment Service
12
service/AppointmentService.java
package com.medicare.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.medicare.dto.AppointmentRequest;
import com.medicare.entity.Appointment;
import com.medicare.entity.AppointmentStatus;
import com.medicare.entity.DoctorProfile;
import com.medicare.entity.PaymentStatus;
import com.medicare.entity.Role;
import com.medicare.entity.User;
import com.medicare.exception.AppointmentConflictException;
import com.medicare.exception.AppointmentNotFoundException;
import com.medicare.exception.DoctorNotFoundException;
import com.medicare.exception.UserNotFoundException;
import com.medicare.repository.AppointmentRepository;
import com.medicare.repository.DoctorProfileRepository;
import com.medicare.repository.UserRepository;@Service
public class AppointmentService {
private final AppointmentRepository appointmentRepository;
private final UserRepository userRepository;
private final DoctorProfileRepository doctorRepository;
public AppointmentService(AppointmentRepository appointmentRepository,
UserRepository userRepository, DoctorProfileRepository doctorRepository) {
this.appointmentRepository = appointmentRepository;
this.userRepository = userRepository;
this.doctorRepository = doctorRepository;
}
public Appointment createAppointment(AppointmentRequest request) {
User patient = userRepository.findById(request.getPatientId())
.orElseThrow(() ->
new UserNotFoundException("Patient not found with id: " +
request.getPatientId()));
if (patient.getRole() != Role.PATIENT) {
throw new IllegalArgumentException(
"Appointment can only be booked for a PATIENT user");
}
DoctorProfile doctor = doctorRepository.findById(request.getDoctorId())
.orElseThrow(() ->
new DoctorNotFoundException("Doctor not found with id: "
+ request.getDoctorId())); boolean slotAlreadyBooked = appointmentRepository
.existsByDoctorIdAndAppointmentDateAndTimeSlot(
doctor.getId(),
request.getAppointmentDate(),
request.getTimeSlot()
);
if (slotAlreadyBooked) {
throw new AppointmentConflictException(
"Selected appointment slot is already booked");
}
Appointment appointment = new Appointment();
appointment.setPatient(patient);
appointment.setDoctor(doctor);
appointment.setAppointmentDate(request.getAppointmentDate());
appointment.setTimeSlot(request.getTimeSlot());
/*
* Consultation fee comes from the doctor's profile.
* We do not trust the frontend to send the fee.
*/
appointment.setConsultationFee(doctor.getConsultationFee());
appointment.setAppointmentStatus(AppointmentStatus.PENDING);
appointment.setPaymentStatus(PaymentStatus.UNPAID);
return appointmentRepository.save(appointment);
} public List<Appointment> getAllAppointments() {
return appointmentRepository.findAll();
}
public Appointment getAppointmentById(Long id) {
return appointmentRepository.findById(id)
.orElseThrow(() ->
new AppointmentNotFoundException("Appointment not found with id: "
+ id));
}
public List<Appointment> getAppointmentsByPatient(Long patientId) {
return appointmentRepository.findByPatientId(patientId);
}
public List<Appointment> getAppointmentsByDoctor(Long doctorId) {
return appointmentRepository.findByDoctorId(doctorId);
}
}Create Appointment Controller
13
controller/AppointmentController.java
package com.medicare.controller;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.medicare.dto.AppointmentRequest;
import com.medicare.entity.Appointment;
import com.medicare.service.AppointmentService;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/appointments")
public class AppointmentController {
private final AppointmentService appointmentService; public AppointmentController(
AppointmentService appointmentService) {
this.appointmentService =
appointmentService;
}
@PostMapping
public ResponseEntity<Appointment>
createAppointment( @Valid @RequestBody AppointmentRequest request) {
Appointment appointment = appointmentService .createAppointment(request);
return ResponseEntity.status(HttpStatus.CREATED).body(appointment);
}
@GetMapping
public ResponseEntity<List<Appointment>>
getAllAppointments() {
return ResponseEntity.ok(appointmentService.getAllAppointments());
}
@GetMapping("/{id}")
public ResponseEntity<Appointment>
getAppointmentById(@PathVariable Long id) {
return ResponseEntity.ok(appointmentService.getAppointmentById(id));
}
@GetMapping("/patient/{patientId}")
public ResponseEntity<List<Appointment>> getAppointmentsByPatient( @PathVariable Long patientId) {
return ResponseEntity.ok(appointmentService.getAppointmentsByPatient(
patientId
)
);
}
@GetMapping("/doctor/{doctorId}")
public ResponseEntity<List<Appointment>>
getAppointmentsByDoctor( @PathVariable Long doctorId) {
return ResponseEntity.ok( appointmentService.getAppointmentsByDoctor(doctorId)
);
}
}Update Global Exception Handler
14
@ExceptionHandler(AppointmentNotFoundException.class)
public ResponseEntity<ErrorResponse>
handleAppointmentNotFound( AppointmentNotFoundException exception,HttpServletRequest request) {
ErrorResponse errorResponse =
new ErrorResponse(LocalDateTime.now(),HttpStatus.NOT_FOUND.value(),
exception.getMessage(), request.getRequestURI() );
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}@ExceptionHandler(AppointmentConflictException.class)
public ResponseEntity<ErrorResponse>
handleAppointmentConflict( AppointmentConflictException exception,
HttpServletRequest request) {
ErrorResponse errorResponse =
new ErrorResponse(LocalDateTime.now(),HttpStatus.CONFLICT.value(),
exception.getMessage(),request.getRequestURI());
return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse);
}409 CONFLICT
Restart and Check Database
15
SHOW TABLES
DESC appointments;Prepare Test Data
16
id
patient_id
doctor_id
appointment_date
time_slot
consultation_fee
appointment_status
payment_status
created_atUser ID 1
Rahul
Role = PATIENTDoctorProfile
ID 1
User: Dr. Ananya Sharma
Specialization: Cardiologist
Fee: ₹1200DoctorProfile
ID 1
User: Dr. Ananya Sharma
Specialization: Cardiologist
Fee: ₹1200Create Appointment
17
POST : http://localhost:8080/api/appointments
{
"patientId": 1,
"doctorId": 1,
"appointmentDate": "2026-09-20",
"timeSlot": "MORNING_10_AM"
}201 CREATED
{ "id": 1,
"patient": {
"id": 1,
"name": "Rahul Shah",
"email": "rahul@example.com",
"phone": "9876543210",
"role": "PATIENT"
},
"doctor": {
"id": 1,
"user": {
"id": 2,
"name": "Dr. Ananya Sharma",
"email": "ananya@example.com",
"phone": "9876543211",
"role": "DOCTOR"
},
"specialization": "Cardiologist",
"experienceYears": 8,
"consultationFee": 1200,
"photoUrl": "https://example.com/ananya.jpg"
},
"appointmentDate": "2026-09-20",
"timeSlot": "MORNING_10_AM",
"consultationFee": 1200,
"appointmentStatus": "PENDING",
"paymentStatus": "UNPAID"
}Check Database
18
SELECT * FROM appointments;id 1 patient 1 doctor 1 date 2026-09-20 slot MORNING_10_AM fee 1200 status PENDING payment UNPAID
19
{
"patientId": 3,
"doctorId": 1,
"appointmentDate": "2026-09-20",
"timeSlot": "MORNING_10_AM"
}Test Duplicate Slot
18
409 CONFLICT
{
"timestamp": "...",
"status": 409,
"message": "Selected appointment slot is already booked",
"path": "/api/appointments"
}
19
Test Invalid Date
18
@FutureOrPresent
400 BAD REQUEST
Appointment date cannot be in the past
20
Try Using DOCTOR as Patient
18
{
"patientId": 2,
"doctorId": 1,
"appointmentDate": "2026-09-20",
"timeSlot": "EVENING_5_PM"
}400 BAD REQUEST
Appointment can only be booked for a PATIENT user
21
Get Appointment
18
GET : http://localhost:8080/api/appointments
200 OK
22
GET : http://localhost:8080/api/appointments/patient/1
Get Patient Appointments
22
GET : http://localhost:8080/api/appointments/doctor/1
Get Patient Appointments
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 completed Lab 6 by connecting patients and doctors through appointment bookings using @ManyToOne and @OneToMany relationships.
Checkpoint
Git Push
git push origin branchNameNext-Lab Preparation
Module:
1) Additional Components in Project
By Content ITV