Enterprise-grade healthcare appointment platform built using Spring Boot microservices, Spring Cloud, event-driven messaging, and full observability.
LuneCare
Traditional appointment systems often become hard to scale when authentication, doctor onboarding, scheduling, payments, and notifications are tightly coupled in a monolith.
This project was built to demonstrate production-style microservice architecture skills for a senior backend role, including:
- service decomposition
- centralized config and discovery
- secure gateway-driven auth
- async domain events
- resilience, observability, and containerized deployment
Deliver a modular healthcare platform where patients can find doctors, book/pay for appointments, receive notifications, and submit feedback while admins can verify doctors.
- faster feature delivery via independent services
- higher reliability via isolation + circuit breakers + retries
- cleaner security boundary through API gateway and internal APIs
- scalability for high-traffic modules (appointments, payments, notifications)
LuneCare supports end-to-end lifecycle:
- patient/doctor registration and login
- doctor onboarding, clinic setup, schedule creation
- slot generation and appointment booking
- payment initiation/verification (Razorpay/Stripe)
- event-driven notifications and refund handling
- feedback workflow after appointment completion
- admin doctor-verification workflow
- Patient registers and logs in.
- Patient searches doctors and views public profile.
- Patient selects clinic/date/slot and books appointment (pending payment).
- Patient pays and verifies payment.
- Appointment becomes confirmed.
- Doctor completes appointment.
- Patient gets feedback eligibility and submits review.
- Identity and auth (
auth) - Patient profile and address (
patient) - Doctor onboarding/clinic/schedule (
doctor) - Appointment and slot orchestration (
appointment) - Payment and refunds (
payment) - Notification center (
notification) - Feedback and ratings (
feedback) - Admin operations (
admin) - Infra: gateway, config server, eureka
- API Gateway: single ingress, JWT validation, rate limiting, header propagation.
- Config Server: centralized externalized config from Git.
- Eureka: service registry/discovery.
- Domain Services: 8 business microservices.
- Datastores:
- PostgreSQL (schema-per-service for relational workloads)
- MongoDB (document model for notification/feedback)
- Redis (token blacklist, gateway rate limiting, slot lock/cache use cases)
- Messaging: RabbitMQ topic exchange for domain events + Spring Cloud Bus refresh.
- Observability: Actuator + Micrometer + Prometheus + Grafana + Loki (+ optional Tempo/OTel).
- Client calls
api-gateway. - Gateway validates JWT (RSA public key), checks blacklist in Redis.
- Gateway injects trusted
X-User-IdandX-User-Role. - Route forwarded using Eureka (
lb://service-name). - Service-level
GatewayAuthFilterbuilds Spring Security context from headers. @PreAuthorizeenforces role access.
- Synchronous (Feign): strong consistency paths (auth->profile creation, payment->appointment confirm, admin aggregation).
- Asynchronous (RabbitMQ): decoupled post-state side effects (notifications, refunds, feedback eligibility).
- Gateway-centric auth avoids duplicate token parsing logic in every service.
- **Internal APIs (
/api/internal/**)** separate service-to-service calls from public APIs. - Event-driven side effects reduce tight runtime coupling.
- Schema-per-service keeps bounded ownership while sharing one Postgres instance in dev.
- Circuit breaker/retry added for external or cross-service calls (Resilience4j).
| Technology | What it is | Why chosen / Problem solved |
|---|---|---|
| Java 21 | LTS JVM runtime | modern language/runtime for enterprise services |
| Spring Boot 3.4.x | app framework | rapid service bootstrap with production features |
| Spring Security | authz framework | role-based endpoint protection |
| Spring Cloud Gateway | reactive API gateway | centralized routing, auth filter, rate limiting |
| Spring Cloud Config | centralized configuration | externalized, versioned configuration management |
| Eureka | service discovery | dynamic service registry for lb:// routing |
| OpenFeign | declarative HTTP clients | simple inter-service sync calls |
| Resilience4j | CB/retry | resilience against downstream failures |
| Spring Data JPA + Hibernate | ORM | relational domain persistence |
| Spring Data MongoDB | document persistence | flexible models for notifications/feedback |
| Flyway | DB migration | versioned schema changes |
| RabbitMQ (AMQP) | message broker | async event flow + Cloud Bus refresh |
| Redis | in-memory data store | token blacklist + gateway rate limiting |
| jjwt | JWT parser/validator | token signature/claims validation |
| springdoc OpenAPI | API docs | self-documenting endpoints via Swagger UI |
| Micrometer + Actuator | metrics/health | standardized telemetry endpoints |
- React 19 + TypeScript + Vite
- React Router
- React Query
- Zustand
- Axios
- Tailwind CSS
- PostgreSQL: auth, patient, doctor, appointment, payment (strong transactional consistency).
- MongoDB: notification, feedback (document-centric, flexible payloads).
- RabbitMQ topic exchange for appointment domain events.
- Spring Cloud Bus over RabbitMQ for distributed config refresh.
- Docker + Docker Compose (dev/local/observability stacks).
- Jib Maven plugin for container image builds.
- Prometheus: metrics scraping
- Grafana: dashboards
- Loki: log aggregation
- Alloy: Docker log shipping to Loki
- Tempo: tracing backend config present (service currently commented in compose)
- OpenTelemetry Java agent: compose/env hints present, optional/in-progress
| Service | Port | Purpose | Database | Key Dependencies |
|---|---|---|---|---|
api-gateway |
8080 | ingress, JWT validation, route+rate limit | Redis | Eureka, auth public key |
config-server |
8888 | centralized config from Git + bus refresh | - | RabbitMQ |
eureka-server |
8761 | service discovery registry | - | config-server |
auth |
8081 | registration/login/token lifecycle/user state | PostgreSQL (auth schema) |
patient, doctor, Redis |
patient |
8082 | patient profile/address/photo | PostgreSQL (patient) |
Cloudinary, doctor (search/public) |
doctor |
8083 | doctor profile, onboarding, clinics, schedules, docs | PostgreSQL (doctor) |
appointment (slot generation), auth |
appointment |
8084 | slots, booking, status transitions | PostgreSQL (appointment) |
doctor, RabbitMQ, Redis |
payment |
8085 | payment initiation/verify/history/refund | PostgreSQL (payment) |
appointment, RabbitMQ, Razorpay/Stripe |
notification |
8086 | user notifications feed | MongoDB (notification_db) |
RabbitMQ |
feedback |
8087 | review/rating workflow | MongoDB (feedback_db) |
RabbitMQ |
admin |
8088 | doctor verification + analytics | none (aggregator) | doctor, auth |
Authorization: Bearer <access_token>for protected APIs.- Gateway injects trusted internal headers:
X-User-Id,X-User-Role.
POST /api/auth/register/patientPOST /api/auth/register/doctorPOST /api/auth/loginPOST /api/auth/refreshPOST /api/users/logoutGET /api/users/me- Internal:
PATCH /api/internal/auth/update-statusGET /api/internal/users/count?role=ROLE_PATIENT|ROLE_DOCTOR|ROLE_ADMIN
Sample request (POST /api/auth/login):
{
"phoneNumber": "9876543210",
"password": "secret123"
}Sample response shape:
{
"success": true,
"status": 200,
"message": "Login Successful",
"data": {
"user": {},
"tokens": {
"accessToken": "...",
"refreshToken": "..."
}
},
"timestamp": "..."
}- Profile:
GET /api/patient/profilePUT /api/patient/profilePATCH /api/patient/profile/upload-photo(multipartfile)DELETE /api/patient/profile/remove-photo
- Address:
POST /api/patient/addressesGET /api/patient/addressesPATCH /api/patient/addressesDELETE /api/patient/addresses
- Internal:
POST /api/internal/patient/create-profile
- Doctor:
GET /api/doctor/profilePUT /api/doctor/profilePATCH /api/doctor/profile/upload-photoDELETE /api/doctor/profile/remove-photoPOST /api/doctor/onboarding/complete
- Clinics:
POST /api/doctor/clinicsGET /api/doctor/clinicsPUT /api/doctor/clinics/{clinicId}DELETE /api/doctor/clinics/{clinicId}
- Clinic schedules:
POST /api/doctor/clinics/{clinicId}/scheduleGET /api/doctor/clinics/{clinicId}/schedulePUT /api/doctor/clinics/{clinicId}/scheduleDELETE /api/doctor/clinics/{clinicId}/schedule/{dayOfWeek}
- Documents:
PATCH /api/doctor/documents/upload
- Public:
GET /api/doctor/searchGET /api/doctor/{doctorId}/public
- Internal:
POST /api/internal/doctor/create-profileGET /api/internal/doctor/clinics/{clinicId}/feesGET /api/internal/doctor/pending-verificationGET /api/internal/doctor/{doctorId}/documentsPATCH /api/internal/doctor/{doctorId}/verification-status
GET /api/appointment/slots/{doctorId}/{clinicId}?date=YYYY-MM-DDPOST /api/appointment/bookGET /api/appointment/{appointmentId}PATCH /api/appointment/{appointmentId}/cancelPATCH /api/appointment/{appointmentId}/completePATCH /api/appointment/{appointmentId}/no-showGET /api/appointment/patient/history?page=0&size=10GET /api/appointment/doctor/todayGET /api/appointment/doctor/history?page=0&size=10- Internal:
POST /api/internal/appointment/slots/generateDELETE /api/internal/appointment/slots/cancel-available/{clinicId}/{dayOfWeek}POST /api/internal/appointment/confirm-paymentGET /api/internal/appointment/{appointmentId}POST /api/internal/appointment/{appointmentId}/release-slot
Sample request (POST /api/appointment/book):
{
"slotId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}POST /api/payment/initiatePOST /api/payment/verifyGET /api/payment/history?page=0&size=10GET /api/payment/appointment/{appointmentId}
Sample request (POST /api/payment/initiate):
{
"appointmentId": "<id>",
"gatewayType": "RAZORPAY"
}Sample request (POST /api/payment/verify, Razorpay):
{
"appointmentId": "<id>",
"razorpayOrderId": "order_x",
"razorpayPaymentId": "pay_x",
"razorpaySignature": "sig_x"
}GET /api/notification?page=0&size=10&isRead=false&type=...&category=...GET /api/notification/unread-countPATCH /api/notification/{notificationId}/readPATCH /api/notification/read-allDELETE /api/notification/{notificationId}DELETE /api/notification/all
POST /api/feedback/doctor/{doctorId}GET /api/feedback/doctor/{doctorId}?page=0&size=10(public)GET /api/feedback/patient/my?page=0&size=10GET /api/feedback/doctor/my?page=0&size=10PUT /api/feedback/{feedbackId}DELETE /api/feedback/{feedbackId}
GET /api/admin/doctors/pendingPATCH /api/admin/doctors/{doctorId}/verifyPATCH /api/admin/doctors/{doctorId}/rejectGET /api/admin/analytics/overview
All services use a consistent error envelope:
{
"success": false,
"error": {
"type": "VALIDATION|BUSINESS|AUTH|...",
"code": "ERROR_CODE",
"message": "Human readable message",
"status": 400,
"timestamp": "...",
"path": "/api/...",
"errors": [
{
"field": "name",
"message": "must not be blank"
}
]
}
}auth -> patient: create patient profile (POST /api/internal/patient/create-profile)auth -> doctor: create doctor profile (POST /api/internal/doctor/create-profile)doctor -> appointment: generate/cancel slots (/api/internal/appointment/...)appointment -> doctor: fetch clinic fees (GET /api/internal/doctor/clinics/{clinicId}/fees)payment -> appointment: get appointment, confirm payment, release slot (/api/internal/appointment/...)admin -> doctor: pending doctors/documents/status updates (/api/internal/doctor/...)admin -> auth: role counts + account status updates (/api/internal/...)doctor -> auth: update account status after onboarding/verification flowpatient -> doctor: doctor search/public profile consumption
- Producer:
appointment - Exchange: configured topic exchange (
rabbitmq.exchange.name) - Routing keys produced:
appointment.confirmedappointment.cancelledappointment.completedappointment.no_showappointment.payment_failed
Consumers:
notificationqueue (appointment.#) -> creates in-app notifications.paymentqueue (appointment.cancelled) -> refund compensation flow.feedbackqueue (appointment.completed) -> feedback eligibility creation.
- Topic exchange from
rabbitmq.exchange.name.
- Notification queue: bound with
appointment.#. - Payment queue: bound with
appointment.cancelled(and in appointment config alsoappointment.confirmedbinding exists for payment queue). - Feedback queue: bound with
appointment.completed.
- Explicit DLQ is not configured currently.
- Consumers catch/log exceptions to avoid poison-message infinite requeue.
- Code comments indicate DLQ is planned future enhancement.
- Appointment status changes.
- Appointment service publishes event to exchange.
- Rabbit routes by routing key.
- Consumers persist notifications, trigger refunds, or grant feedback eligibility.
- User logs in via
authand receives access + refresh tokens. - Access token signed with RSA private key (auth service).
- API gateway validates token using RSA public key.
- Gateway checks token
jtiblacklist in Redis.
- Gateway injects trusted identity headers.
- Service
GatewayAuthFilterbuildsSecurityContext. - Endpoint-level
@PreAuthorizeenforces role access.
- Public routes: registration/login/refresh, doctor public search/profile, health/info/docs.
- Internal endpoints blocked externally at gateway (
/internal/forbidden from outside). - Logout invalidates tokens through blacklist flow.
- token validation + claim checks (
sub,role,jti) - anti-header-spoofing (removes client supplied
X-User-*, adds verified values) - Redis rate limiting with route-specific policies (auth/payment/admin stricter/adjusted)
auth:users,refresh_tokenpatient:patients,addressdoctor:doctors,doctor_profiles,doctor_documents,clinics,clinic_schedules,doctor_languagesappointment:appointments,slotspayment:payment_records,payment_gateway_details
notification_db.notificationsfeedback_db.feedbacksfeedback_db.feedback_eligibility
users (auth)1->1 logical withpatients/doctorsdoctors1->manyclinicsclinics1->manyclinic_schedulesdoctors1->1doctor_profilesdoctors1->manydoctor_documentsappointmentsreferencesslot, patient, doctor, clinic IDspayment_records1->1payment_gateway_detailsfeedback_eligibilityandfeedbackskeyed byappointmentId
- Services import config via
spring.config.import=configserver:http://localhost:8888. - Config server sources from Git repo (
lune-care-config). - Profiles: primarily
dev, withdockeroverlay in containerized runtime. - Cloud Bus refresh endpoint available (
/actuator/busrefresh) to propagate config updates. - Encryption support in config server (
encrypt.key) for encrypted{cipher}values. - Secret handling expectation:
- keep
.envout of VCS - inject via environment variables/secrets manager in prod
- keep RSA keys under mounted secret path
- keep
infra.yaml: RabbitMQ, Redisdatabases.yaml: PostgreSQL, MongoDBservices.yaml: config/eureka/all app services/gatewayobservability-and-monitoring.yaml: Prometheus/Grafana/Loki/Alloy (+ Tempo config)
infra -> db -> config-server -> eureka -> business services -> api-gateway
- shared bridge network:
lunecare_network - inter-container DNS by service names
- persistent data for PostgreSQL, MongoDB, RabbitMQ, Redis
- mounted
init-db.sql - mounted RSA key directory into auth and gateway containers
- configured for most infra/services using actuator or native commands (
pg_isready,redis-cli ping, etc.)
- All services expose
/actuator/prometheus. - Prometheus scrapes gateway, infra services, and all domain services.
- Structured service logs shipped by Grafana Alloy from Docker daemon.
- Loki stores centralized logs.
- Grafana can query logs and correlate with traces (Tempo datasource wired).
- Tempo config present.
- OTel Java agent config scaffold present in compose (
JAVA_TOOL_OPTIONS, OTLP endpoint), but rollout is partial/commented in places.
- Grafana datasources provisioned for Prometheus/Loki/Tempo.
- Alerting rules are not explicitly defined in current repo.
infrastructure/
api-gateway/
config-server/
eureka-server/
services/
auth/
patient/
doctor/
appointment/
payment/
notification/
feedback/
admin/
docker/
docker-compose/
dev/
local/
observability/
secrets/
public/
web/
infrastructure/*: platform-level servicesservices/*: business microservicesdocker/docker-compose/dev: fully containerized environmentdocker/docker-compose/local: infra-only for running services from IDEdocker/docker-compose/observability: monitoring/logging stack configsweb: React frontend client
- Layered architecture (controller -> service -> repository)
- DTO mapping (
payload/request,payload/response, mapper classes) - Saga-style choreography for payment/refund lifecycle
- Event-driven architecture with topic routing
- Idempotency guards (feedback eligibility, payment state checks)
- Circuit breaker + retry (Resilience4j)
- Optimistic locking (
@Version) in appointment/slot entities - Standardized response/error wrapper
- Centralized exception handling per service
- Security boundary with gateway + internal API segregation
- Java 21
- Maven 3.9+
- Docker Desktop
- Node 20+ (for frontend)
cd docker/docker-compose/dev
cp .env.example .env.dev
make upAccess:
- API Gateway:
http://localhost:8080 - Eureka:
http://localhost:8761 - Grafana:
http://localhost:3000 - Prometheus:
http://localhost:9090 - RabbitMQ UI:
http://localhost:15672
cd docker/docker-compose/local
make up-dbThen start services in this order:
- config-server
- eureka-server
- auth, patient, doctor, appointment, payment, notification, feedback, admin
- api-gateway
cd web
npm install
npm run dev- Basic Spring Boot context tests present in all services (
*ApplicationTests). - Utility test file present in auth (
PasswordGenerator).
# service-wise
cd services/auth && mvn test
cd services/appointment && mvn test
# repeat for others- Use Postman/Insomnia through gateway (
localhost:8080). - Authenticate first and reuse bearer token for protected flows.
- Validate async side effects (notifications/refunds/feedback eligibility) by checking respective service data stores.
- Add DLQ + retry/backoff policy for RabbitMQ consumers.
- Complete OpenTelemetry rollout with distributed trace propagation across all services.
- Add contract tests for Feign/internal APIs and event schemas.
- Introduce CI pipeline for unit/integration/security checks.
- Add Testcontainers-based integration tests for Postgres/Mongo/Rabbit/Redis.
- Externalize secrets to vault/secret manager for production.
- Add Kubernetes manifests/Helm charts and HPA policy for scale-out.
- Add idempotency keys for payment initiation requests at API level.
- Auth:
/api/auth/*,/api/users/* - Patient:
/api/patient/profile,/api/patient/addresses - Doctor:
/api/doctor/*(including/search,/{id}/public) - Appointment:
/api/appointment/* - Payment:
/api/payment/* - Notification:
/api/notification/* - Feedback:
/api/feedback/* - Admin:
/api/admin/*
/api/internal/auth/*/api/internal/users/*/api/internal/patient/*/api/internal/doctor/*/api/internal/appointment/*
Note: Gateway blocks external access to internal endpoints.
