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