Clinic Management System Development with Microservices
Posted By : Ankit Mishra | 30-Mar-2026
Monolithic clinic management systems struggle under real-world healthcare load ? a bug in billing can crash the entire patient portal, and scaling one module means scaling everything. Microservices architecture, paired with Docker and Kubernetes, solves this by breaking the system into independent, deployable units.
In this guide, we walk through architecting and deploying a clinic management system with services for appointments, patient records, billing, and notifications , and show exactly how each user journey maps to the underlying services and code.
System Architecture Overview
The system is split into five core services, each owning its own database and communicating via REST (synchronous) or RabbitMQ (asynchronous events):
- Auth Service ? JWT-based authentication backed by Redis
- Appointment Service ? Books, reschedules, and cancels appointments; publishes events to RabbitMQ
- Patient Records Service ? Stores and retrieves sensitive medical data with strict access control
- Billing Service ? Handles invoices and insurance integrations; listens to appointment events
- Notification Service ? Stateless consumer that sends email/SMS alerts on appointment events
All external traffic routes through an NGINX Ingress controller, which handles TLS termination, rate limiting, and path-based routing to the appropriate service.
Prerequisites
- Docker v24+ and Docker Compose v2+
- kubectl v1.28+ and a running Kubernetes cluster (Minikube locally, or GKE/EKS/AKS for production)
- Node.js v20+
User Journey Flow
Three primary actors interact with the system, Patient, Doctor, and Admin. Each step in the journey maps directly to a service, an API call, and a code snippet below.
Also, Read | FHIR and Blockchain | A New Age of Healthcare Data Management
Journey 1 : Patient Books an Appointment
[Patient Opens App]
?
?
[Auth Service] ???? POST /auth/login
? JWT token issued, stored in Redis
?
[Appointment Service] ???? GET /appointments/available
? Returns available doctor slots
?
[Patient Selects Slot] ???? POST /appointments
? Conflict check + insert as 'confirmed'
?
???? [Notification Service] ?? Confirmation email/SMS sent
???? [Billing Service] ?????? Pending invoice created
Step 1 : Issue JWT on login:
const token = jwt.sign(
{ patientId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '8h' }
);
await redis.set(`session:${user.id}`, token, 'EX', 28800);
Step 2 : Conflict check + book appointment:
const conflict = await pool.query(
`SELECT id FROM appointments
WHERE doctor_id = $1 AND scheduled_at = $2 AND status != 'cancelled'`,
[doctorId, scheduledAt]
);
if (conflict.rows.length > 0)
return res.status(409).json({ error: 'Time slot already booked' });
const { rows } = await pool.query(
`INSERT INTO appointments (patient_id, doctor_id, scheduled_at, status)
VALUES ($1, $2, $3, 'confirmed') RETURNING *`,
[req.user.patientId, doctorId, scheduledAt]
);
// Triggers both Notification and Billing services
channel.sendToQueue('appointment.booked',
Buffer.from(JSON.stringify({ appointmentId: rows[0].id, patientId: rows[0].patient_id })),
{ persistent: true }
);
Journey 2 : Doctor Reviews Patient Before Visit
[Doctor Logs In]
?
?
[Auth Service] -> POST /auth/login (role: 'doctor' in JWT)
?
?
[Appointment Service] -> GET /appointments?doctorId=xxx&date=today
?
?
[Patient Records Service] -> GET /records/:patientId
? Returns history, diagnoses, prescriptions
?
[Doctor Updates Notes] -> PATCH /records/:patientId
?
?
[Appointment Service] -> PATCH /appointments/:id ? status: 'completed'
?
???? [Billing Service] -> Invoice status updated to 'due'
???? [Notification Service] -> Post-visit summary sent to patient
Step 3 : Fetch today's schedule:
const { rows } = await pool.query(
`SELECT * FROM appointments
WHERE doctor_id = $1
AND scheduled_at::date = CURRENT_DATE
AND status = 'confirmed'`,
[req.user.doctorId]
);
Step 4 ? Mark visit complete + trigger billing and notification:
await pool.query(
`UPDATE appointments SET status = 'completed' WHERE id = $1`,
[appointmentId]
);
channel.sendToQueue('appointment.completed',
Buffer.from(JSON.stringify({ appointmentId, patientId })),
{ persistent: true }
);
Also, Explore | Revolutionizing Healthcare With AI As A Virtual Nursing Assistant
Journey 3 : Admin Manages Billing
[Admin Logs In]
?
?
[Auth Service] -> POST /auth/login (role: 'admin' in JWT)
?
?
[Billing Service] -> GET /billing/invoices?status=pending
?
??? PATCH /billing/invoices/:id (manual adjustment)
??? POST /billing/claims (submit to insurer)
?
?
[External Insurance API]
?
??? approved / rejected / partial
?
?
[Billing Service] -> Invoice updated
?
???? [Notification Service] ?? Patient notified
Step 5 : Submit insurance claim + update invoice:
const claim = await insurerAPI.submitClaim({ invoiceId, patientId, amount });
await pool.query(
`UPDATE invoices SET status = $1 WHERE id = $2`,
[claim.status, invoiceId]
);
channel.sendToQueue('billing.updated',
Buffer.from(JSON.stringify({ patientId, status: claim.status })),
{ persistent: true }
);
Journey 4 : Patient Cancels an Appointment
[Patient Opens App]
?
?
[Auth Service] -> Token validated from Redis
?
?
[Appointment Service] -> DELETE /appointments/:id
? Ownership check: patient_id must match JWT
? Status updated to 'cancelled'
?
???? [Notification Service] -> Cancellation confirmation sent
???? [Billing Service] -> Pending invoice voided
Step 6 ? Cancel with ownership check + void invoice:
const { rows } = await pool.query(
`UPDATE appointments SET status = 'cancelled'
WHERE id = $1 AND patient_id = $2 RETURNING *`,
[appointmentId, req.user.patientId]
);
if (rows.length === 0)
return res.status(403).json({ error: 'Not authorized or not found' });
await pool.query(
`UPDATE invoices SET status = 'voided' WHERE appointment_id = $1`,
[appointmentId]
);
channel.sendToQueue('appointment.cancelled',
Buffer.from(JSON.stringify({ appointmentId, patientId: req.user.patientId })),
{ persistent: true }
);
Notification Service : Consumes All Journey Events
Every journey above ends with an event on RabbitMQ. The notification service handles all of them in one place:
channel.prefetch(1);
const queues = ['appointment.booked', 'appointment.completed',
'appointment.cancelled', 'billing.updated'];
queues.forEach(queue => {
channel.consume(queue, async (msg) => {
try {
const data = JSON.parse(msg.content.toString());
await sendNotification(queue, data);
channel.ack(msg);
} catch (err) {
channel.nack(msg, false, true); // requeue on failure
}
});
});
Also, Discover | Application of Blockchain and IoT (Internet of Things) in Healthcare
Deploying on Docker and Kubernetes
Containerizing Services
Each service uses a multi-stage Dockerfile , non-root user, minimal image, direct node execution for proper signal handling:
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
CMD ["node", "src/index.js"]
Kubernetes Health Probes
Liveness and readiness probes prevent Kubernetes from routing traffic to pods that are still starting or have silently crashed:
livenessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 15
readinessProbe:
httpGet:
path: /health
port: 3001
initialDelaySeconds: 5
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
Autoscaling
The appointment service scales between 3 and 15 pods automatically based on CPU load:
minReplicas: 3
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
Graceful Shutdown
Every service handles SIGTERM to finish in-flight requests before shutting down:
process.on('SIGTERM', async () => {
server.close(async () => {
await pool.end();
await amqpConn.close();
process.exit(0);
});
});
Secrets Management
Credentials always come from Kubernetes Secrets , never hardcoded or stored in ConfigMaps:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: clinic-secrets
key: APPT_DB_PASSWORD
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: clinic-secrets
key: JWT_SECRET
Developer Best Practices
Never share databases between services. If billing reads directly from the appointments table, you have a hidden monolith. All cross-service data access must go through the owning service's API.
Use persistent: true for all critical events. Appointment bookings, completions, cancellations, and billing updates must survive a RabbitMQ restart.
Set resource requests and limits on every pod. Without them, one misbehaving service can starve others on the same node , a serious risk in a healthcare system.
Conclusion
Each user journey in this clinic system ? booking, visiting, billing, and cancelling ? maps cleanly to a set of services, API calls, and RabbitMQ events. No service knows about another's internals. The appointment booking alone touches four services without any tight coupling.
Start with Docker Compose locally to validate inter-service events, then deploy to Kubernetes with health probes, autoscaling, and proper secrets management. The architecture pays off quickly as your clinic system grows in users, features, and compliance requirements. For more information related to healthcare app development, connect with our healthcare developers.
Cookies are important to the proper functioning of a site. To improve your experience, we use cookies to remember log-in details and provide secure log-in, collect statistics to optimize site functionality, and deliver content tailored to your interests. Click Agree and Proceed to accept cookies and go directly to the site or click on View Cookie Settings to see detailed descriptions of the types of cookies and choose whether to accept certain cookies while on the site.
About Author
Ankit Mishra
Ankit Mishra is an experienced backend developer with 2.5 years of industry expertise. He is well-versed in using Nodejs, Express, Sequelize, PHP, Laraval, Apache, HTML, CSS, JavaScript, jQuery, and various databases, and stays up-to-date with the latest technologies. Ankit has worked on various projects, including Oodles Blockchain internal site, Duel Deal, and Distincts, and is always eager to explore new technologies and think critically. His creative and analytical abilities make him a valuable asset to any organization he works with.