A production-ready, secure, and scalable RESTful API for a Smart Healthcare Appointment & Records System, built with Java 17 + Spring Boot 3.5. Implements real-world engineering practices including JWT-based auth, role-based access control, centralized exception handling, request validation, OAuth2 social login, Razorpay payment gateway integration, billing management, Cloudinary-backed cloud file storage, and automated API documentation.
Frontend Repository: π HealthCare-Frontend β React 19 | Vite | Redux Toolkit | Tailwind CSS | Razorpay Checkout
I've recorded short walkthroughs breaking down some of the trickier features and bugs in this project β not just showing that it works, but explaining the reasoning behind the implementation.
| # | Topic | Link |
|---|---|---|
| 1 | π Notification Feature β Overview | βΆ Watch |
| 2 | π§© Notification Feature β Service & Repository Layer | βΆ Watch |
| 3 | β In-App Notification Feature β Completed Walkthrough | βΆ Watch |
| 4 | π JWT Authentication Bug Fix β Root Cause & Resolution | βΆ Watch |
| 5 | β Bean Validation & Global Exception Handler | βΆ Watch |
| 6 | π§ OTP-Based Registration Flow | βΆ Watch |
| 7 | π OTP Email-Based Password Reset | βΆ Watch |
π‘ These videos are meant to give reviewers a look into my thought process β how I debug, design, and reason through real backend problems, not just the final code.
| Feature | Details |
|---|---|
| π JWT Auth + Role-Based Access | Stateless authentication with role-scoped endpoints (ADMIN / DOCTOR / PATIENT) |
| π§ Email OTP Verification | 6-digit OTP sent via email before registration; 10-minute expiry, single-use, auto-cleared on resend |
| π Google OAuth2 Social Login | Patients and doctors can sign in with Google via Spring OAuth2 client |
| π³ Razorpay Payment Gateway | Real, verified online payments for appointment billing β UPI, Cards & Netbanking, with server-side signature verification |
| βοΈ Cloudinary Cloud Media Storage | Profile pictures uploaded via multipart requests are validated, streamed, and persisted to Cloudinary β no local disk dependency, fully production-portable |
| π‘οΈ Global Exception Handler | @RestControllerAdvice catches all exceptions β validation, auth, not-found, duplicates β and returns consistent JSON error responses with timestamp |
| β Bean Validation | @Valid + Jakarta Validation annotations (@NotNull, @NotBlank, @Email, @Digits) on all request DTOs |
| π©Ί Doctor Approval Workflow | Doctors register but are locked out until an Admin approves their account |
| π Password Reset Flow | Forgot-password β token generation β reset-password via secure token |
| π° Billing & Revenue Module | Appointments auto-generate billing records; Admin can view daily/monthly revenue stats |
| π Role-Aware File Management | Multipart profile picture upload/retrieval shared across PATIENT and DOCTOR roles, backed by Cloudinary |
| π Real-time Appointment Notifications | Dual-channel notifications (in-app + email) for appointment creation & status tracking; includes time & reason details |
| π¬ Notification Entity | In-app notification system with read/unread tracking and multi-type support (Appointment, Prescription, Payment, Registration) |
| π Swagger / OpenAPI Docs | Auto-generated interactive API docs via SpringDoc OpenAPI 2.5 |
| π CORS Configured | Whitelisted for React frontend at localhost:5173 and localhost:3000 via allowedOriginPatterns, safely combined with credentialed requests |
| β‘ Stateless Sessions | SessionCreationPolicy.STATELESS β no server-side session state |
To maintain professional-grade data traceability, the system implements JPA Auditing.
- Automatic Metadata: Every core entity automatically records when it was created/modified and who performed the action.
- Traceability: Integrated with Spring Security to capture the currently logged-in user via
AuditorAware. - Implementation: Utilizes
@MappedSuperclasswithBaseAuditEntityto eliminate boilerplate, ensuring consistentcreated_at,updated_at,created_by, andupdated_byfields across the entire database schema.
Classic 3-tier layered architecture:
Controller (REST API) β Service (Business Logic) β Repository (JPA / MySQL)
The codebase is organized by domain modules (feature-based packaging), not by layer β keeping related code co-located and the project scalable.
com.ankit.HealthCare_Backend/
βββ appointment/ # Appointment booking, status updates
βββ authentication/ # JWT, OAuth2, Security config, Auth endpoints
βββ billing/ # Billing records, payment, revenue stats, Razorpay integration
βββ communication/ # Contact Us feature
βββ core/ # Shared enums (AppointmentStatus, BillingStatus), Role entity
βββ Exception/ # GlobalExceptionHandler + custom exceptions
βββ filemanagement/ # Profile picture upload/retrieval, Cloudinary integration
βββ Notification/ # Notification entity & repository
βββ prescription/ # Doctor prescriptions
βββ usermanagement/ # Admin, Doctor, Patient, User, Profile sub-modules
| Component | Technology | Version |
|---|---|---|
| Framework | Spring Boot | 3.5.7 |
| Security | Spring Security + JWT (jjwt) | 6.5.7 / 0.11.5 |
| Social Login | Spring OAuth2 Client (Google) | 6.5.7 |
| Payment Gateway | Razorpay Java SDK | Latest stable |
| Media Storage | Cloudinary Java SDK | Latest stable |
| Spring Boot Starter Mail (JavaMailSender) | 3.5.7 | |
| ORM | Spring Data JPA + Hibernate | 3.5.7 |
| Database | MySQL (mysql-connector-j) | 8.3.0 |
| Validation | Spring Boot Starter Validation (Jakarta) | 3.5.7 |
| API Docs | SpringDoc OpenAPI (Swagger UI) | 2.5.0 |
| Build | Maven | 3.x |
| Utilities | Lombok | 1.18.32 |
| Language | Java | 17 |
Request β JwtFilter β Validate Token β Set SecurityContext β @PreAuthorize / hasRole()
- Registration β
POST /api/auth/registerwith full Bean Validation (@Valid) - Login β
POST /api/auth/loginreturns a signed JWT; doctors blocked until approved - Google OAuth2 β
/oauth2/**flow handled byOAuth2LoginSuccessHandler, redirects with token - JWT Filter β
JwtFilterintercepts every request, validates signature & expiry - Role Guards β
/api/patient/**βROLE_PATIENT,/api/doctor/**βROLE_DOCTOR,/api/admin/**βROLE_ADMIN,/api/profile/**βROLE_PATIENTorROLE_DOCTORviahasAnyRole - Email OTP β
POST /api/auth/send-otpsends a 6-digit OTP;POST /api/auth/verify-otpvalidates it before allowing registration - Password Reset β Secure time-limited token flow via
POST /api/auth/forgot-passwordβPOST /api/auth/reset-password - BCrypt β All passwords hashed with
BCryptPasswordEncoder - Payment Signature Verification β Every Razorpay payment is verified server-side via HMAC signature before billing status changes β the client can never self-report a payment as successful
- Credential-Safe CORS β
CorsConfigurationSourceusesallowedOriginPatterns(never a bare"*") so credentialed requests (JWT-bearing) from the frontend are honored without violating the CORS spec
Profile pictures for both Patients and Doctors are uploaded directly to Cloudinary rather than local disk β meaning the API remains stateless and horizontally scalable (no shared filesystem needed across instances), and images are served from Cloudinary's CDN.
| Capability | Status |
|---|---|
Multipart image upload (multipart/form-data) |
β Implemented |
Content-type validation β only image/* accepted |
β Implemented |
| File size validation β 5MB max, rejected before upload | β Implemented |
| Direct stream-to-Cloudinary upload (no local temp storage) | β Implemented |
Role-aware persistence β updates Patient or Doctor entity based on logged-in user's role |
β Implemented |
| Returns CDN-backed image URL in response for immediate frontend use | β Implemented |
1. Client sends multipart request βββΆ POST /api/profile/upload-image (field: profilePicture)
2. JwtFilter authenticates βββΆ Principal resolved to logged-in user
3. ProfileService validates file βββΆ content-type check + 5MB size check
4. CloudinaryService.uploadImage() βββΆ streams file directly to Cloudinary, returns secure URL
5. Service resolves role βββΆ PATIENT β Patient entity, DOCTOR β Doctor entity
6. profilePicture column updated βββΆ persisted via @Transactional save
7. Response βββΆ { success, message, imageUrl }
Because the upload is wrapped in @Transactional, a failure at any stage (invalid file, Cloudinary error, DB save error) rolls back cleanly rather than leaving a partially-updated profile.
The billing module integrates Razorpay end-to-end for appointment payments β not a mocked checkout, but a real gateway integration with proper order lifecycle and server-side trust boundaries.
| Capability | Status |
|---|---|
Order creation via backend (POST /api/patient/payments/create-order) |
β Implemented |
| Razorpay Checkout (UPI, Cards, Netbanking) | β Implemented |
| Server-side payment signature verification (HMAC-SHA256) | β Implemented |
Real-time billing status sync (UNPAID β PAID) |
β Implemented |
| Payment failure & checkout-dismissal handling | β Implemented |
| Revenue reporting (daily / monthly) | β Implemented |
A naive integration trusts the frontend to say "payment succeeded." This one doesn't. The flow is:
1. Frontend requests an order βββΆ Backend calls Razorpay Orders API, returns order_id
2. Razorpay Checkout opens βββΆ User pays via UPI / Card / Netbanking
3. Razorpay returns βββΆ payment_id, order_id, signature (to frontend)
4. Frontend forwards these βββΆ Backend verification endpoint
5. Backend recomputes HMAC βββΆ using Razorpay key secret
6. Only on signature match βββΆ Billing status flips to PAID
This is the same trust model used by real fintech and healthtech platforms β the backend is the single source of truth for what counts as a successful payment, never the client.
Razorpay's Test Mode sandbox reproduces the entire checkout, OTP, and verification flow with zero real money movement β used to validate this integration end-to-end.
Card Payment
| Field | Test Value |
|---|---|
| Card Number | 4111 1111 1111 1111 (Visa) or 5267 3181 8797 5449 (Mastercard) |
| Expiry (MM/YY) | Any future date β e.g. 12/30 |
| CVV | Any 3 digits β e.g. 123 |
| Cardholder Name | Any name |
| OTP | Any 4β10 digit number β e.g. 1234 |
Select Success on Razorpay's mock bank page to complete the simulated transaction, or use card 4000 0000 0000 0002 and select Failure to test the failure path.
UPI Payment
| Field | Test Value |
|---|---|
| UPI ID | success@razorpay |
No need to scan the on-screen QR with a real device β that only resolves against live, NPCI-registered transactions. Entering the test UPI ID simulates an instant successful payment in sandbox mode.
Going live requires only swapping the test key (rzp_test_...) for a live key (rzp_live_...) post KYC/activation on Razorpay's dashboard β no changes to the integration logic itself.
A single @RestControllerAdvice class handles all error scenarios and returns a consistent JSON error envelope:
{
"timestamp": "2025-10-27T14:32:10.123",
"status": 404,
"message": "Doctor with id 5 not found"
}| Exception | HTTP Status |
|---|---|
ResourceNotFoundException |
404 Not Found |
DuplicateResourceException |
409 Conflict |
UnauthorizedException |
401 Unauthorized |
IllegalArgumentException |
400 Bad Request |
MethodArgumentNotValidException |
400 Bad Request (validation errors) |
PaymentVerificationException |
400 Bad Request (Razorpay signature mismatch) |
Exception (fallback) |
500 Internal Server Error |
All incoming request DTOs are validated with Jakarta Bean Validation annotations before reaching the service layer:
// RegisterRequestDTO example
@NotNull @NotBlank @Email private String email;
@NotNull @NotBlank private String password;
@Digits(integer=10, fraction=0) private Long contactNumber;
// AppointmentDTO example
@NotNull private Long patientId;
@NotNull private Long doctorId;
@NotNull private LocalDate appointmentDate;
// PaymentVerificationDTO example
@NotNull @NotBlank private String razorpayOrderId;
@NotNull @NotBlank private String razorpayPaymentId;
@NotNull @NotBlank private String razorpaySignature;
@NotNull private Long appointmentId;Validation failures are caught by the Global Exception Handler and returned as structured 400 Bad Request responses.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /send-otp |
Send 6-digit OTP to email for verification | Public |
| POST | /verify-otp |
Verify the OTP before registration | Public |
| POST | /register |
Register a new user (Patient/Doctor) | Public |
| POST | /login |
Authenticate and receive JWT | Public |
| POST | /forgot-password |
Request a password reset token | Public |
| POST | /reset-password |
Reset password using token | Public |
| GET | /oauth2/callback |
Google OAuth2 redirect handler | Public |
| Method | Endpoint | Description |
|---|---|---|
| GET | /doctors |
Browse all approved doctors |
| POST | /appointments/new |
Book a new appointment |
| GET | /appointments/my |
View personal appointment history |
| DELETE | /appointments/{id}/cancel |
Cancel an appointment |
| PUT | /appointments/{id}/pay |
Make payment for an appointment |
| POST | /payments/create-order |
Create a Razorpay order for an appointment |
| POST | /payments/verify |
Verify Razorpay payment signature and mark billing as PAID |
| GET | /prescriptions |
View personal prescriptions |
| GET | /profile |
View own patient profile (includes profilePicture URL) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /profile |
View own doctor profile (includes profilePicture URL) |
| GET | /appointments/my |
View all own appointments |
| PUT | /appointments/{id}/status |
Update appointment status (triggers in-app + email notifications) |
| GET | /patients |
View all own patients |
| POST | /prescription |
Create a prescription |
| GET | /prescriptions |
View all own prescriptions |
| Method | Endpoint | Description |
|---|---|---|
| POST | /upload-image |
Upload a profile picture (multipart/form-data, field: profilePicture) β validated, streamed to Cloudinary, and linked to the caller's Patient or Doctor record |
| DELETE | /delete-image |
Remove the current profile picture |
| Method | Endpoint | Description |
|---|---|---|
| GET | /doctors |
Get all doctors (including pending) |
| PUT | /doctors/{id}/approve |
Approve a doctor |
| PUT | /doctors/{id}/reject |
Reject / revoke a doctor |
| GET | /patients |
Get all patients |
| GET | /billing |
View all billing records |
| PUT | /billing/{id}/status |
Update a billing record's status |
| GET | /revenue/daily |
Get today's total revenue |
| GET | /revenue/monthly |
Get current month's total revenue |
| Method | Endpoint | Description |
|---|---|---|
| GET | /my |
Get all notifications for logged-in user (newest first) |
| GET | /unread-count |
Get count of unread notifications (for UI badge) |
| PUT | /{id}/read |
Mark a specific notification as read |
| PUT | /mark-all-read |
Mark all unread notifications as read |
Core entities: User, Role, Patient, Doctor, Admin, Appointment, Prescription, Billing, ContactUs, Notification, PasswordResetToken
Billing stores the Razorpay orderId, paymentId, and status (UNPAID / PAID) per appointment, giving a full payment audit trail per record.
Patient and Doctor each store a profilePicture column holding the Cloudinary-hosted CDN URL of the user's uploaded profile image.
Once running, the interactive Swagger UI is available at:
http://localhost:8080/swagger-ui/index.html
- Java 17+
- Maven 3.x
- MySQL 8.x
- A Razorpay account (Test Mode keys are free β no business verification needed to start testing)
- A Cloudinary account (free tier is sufficient for development)
git clone https://github.com/ankitdoi-coder/healthcare-backend.git
cd healthcare-backendCreate the database:
CREATE DATABASE healthcaredb;Configure src/main/resources/application.properties:
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
app.jwt.secret=${JWT_SECRET}
app.jwt.expiration=${JWT_EXPIRATION_MS}
razorpay.key.id=${RAZORPAY_KEY_ID}
razorpay.key.secret=${RAZORPAY_KEY_SECRET}
cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME}
cloudinary.api-key=${CLOUDINARY_API_KEY}
cloudinary.api-secret=${CLOUDINARY_API_SECRET}Run:
mvn spring-boot:runServer starts at http://localhost:8080
| Variable | Description | Example |
|---|---|---|
DB_URL |
JDBC connection URL | jdbc:mysql://localhost:3306/healthcaredb |
DB_USERNAME |
Database username | root |
DB_PASSWORD |
Database password | your_password |
JWT_SECRET |
Secret key for signing JWTs | a-very-long-random-secret-key |
JWT_EXPIRATION_MS |
Token TTL in milliseconds | 86400000 (24h) |
RAZORPAY_KEY_ID |
Razorpay API Key ID (test or live) | rzp_test_xxxxxxxxxxxx |
RAZORPAY_KEY_SECRET |
Razorpay API Key Secret (test or live) | your_razorpay_key_secret |
CLOUDINARY_CLOUD_NAME |
Cloudinary account cloud name | your_cloud_name |
CLOUDINARY_API_KEY |
Cloudinary API key | 123456789012345 |
CLOUDINARY_API_SECRET |
Cloudinary API secret | your_cloudinary_secret |
The system sends dual-channel notifications (in-app + email) for all key appointment events. Notifications include appointment time and reason details for full context.
When a patient books an appointment:
In-App Notification (stored in database)
- Sent to: Patient & Doctor
- Message: "Your Appointment Booked with Doctor: [Name]" (Patient) / "You have new Appointment from Patient: [Name]" (Doctor)
- Type:
APPOINTMENT - Read/Unread tracking enabled
Email Notification (via JavaMailSender)
- Patient receives: Appointment confirmation with doctor name, date, and gratitude message
- Doctor receives: Appointment alert with patient name, scheduled date, and dashboard reminder
- Both emails include appointment time (
LocalTime) and reason for visit details
When a doctor updates the appointment status (SCHEDULED β COMPLETED / CANCELED, etc.):
In-App Notification (to patient)
- Message: "Your appointment status has been updated to: [STATUS] by Dr. [Name]"
- Type:
APPOINTMENT
Email Notification (to patient)
- Subject: "Appointment Status Update"
- Contains: Appointment date, doctor name, new status, and dashboard link
Patients and doctors can:
- Retrieve all notifications sorted by creation date (newest first)
- Check unread notification count (for UI bell badge)
- Mark individual notifications as read
- Mark all notifications as read in one call
APPOINTMENTβ Appointment booking and status changesPRESCRIPTIONβ Prescription-related (extensible for future use)PAYMENTβ Payment status updates (extensible for future use)REGISTRATIONβ Account registration events (extensible for future use)
This project was built to demonstrate practical, production-grade backend engineering rather than tutorial-level CRUD:
- π Security-first payment handling β billing status is never trusted from the client; it's gated behind server-side HMAC signature verification, mirroring real fintech/healthtech systems.
- βοΈ Stateless media handling β profile pictures stream directly to Cloudinary rather than local disk, keeping the API instance-agnostic and production-portable from day one.
- πͺͺ Stateless, role-scoped JWT auth with a proper OAuth2 social login path alongside it.
- π§© Consistent error contracts across the entire API via a single global exception handler.
- ποΈ Domain-driven package structure that scales cleanly as features are added, rather than a flat MVC layout.
- π Real third-party integration experience with Razorpay's order lifecycle (create β checkout β verify) and Cloudinary's upload API β not simulated or mocked integrations.
- π₯ Documented engineering process β video walkthroughs above show real debugging and design decisions, not just polished final output.

