
Microservices Healthcare Platform on Kubernetes
MediCare is a microservices healthcare platform built with Spring Boot, React, and Kubernetes, handling patient records, appointments, telemedicine, and billing.
Table of Contents
Building MediCare: A Microservices Healthcare Platform on Kubernetes
Healthcare software has a specific kind of pressure that most side projects do not. Patient records, appointment bookings, billing, and telemedicine sessions cannot fail quietly, and they cannot be bolted together as one large application without the whole system becoming fragile the moment any single part changes. MediCare was built around that constraint from the start, as a polyglot monorepo made up of eight independent backend services sitting behind a single API gateway, deployed through Kubernetes rather than a single server.
This post walks through the reasoning behind the architecture, not just what the stack is, but why it is shaped this way, where the harder engineering problems actually showed up, and what tradeoffs came with each decision.
Why Split Into Eight Services
The backend is horizontally sliced by domain, not by technical layer. Each service owns one piece of the healthcare workflow and nothing else:
| Service | Port | Responsibility |
|---|---|---|
| Auth | 3001 | Authentication and JWT issuing across the system |
| Patient | 3002 | Patient demographic data and records |
| Doctor | 3003 | Doctor credentials, specialties, availability |
| Appointment | 3004 | Booking, cancellation, state synchronization |
| Telemedicine | 3005 | Remote video consultation management |
| Payment | 3006 | Invoicing, transaction state, billing history |
| Notification | 3007 | Webhooks, email, and SMS alerts |
| AI Symptom | 3008 | Symptom diagnostics through third party AI integration |
The reasoning behind this split is mostly about risk containment. A bug in the notification service should never be able to take down appointment booking, and a spike in telemedicine traffic should not slow down billing. In a monolith, all of that logic shares memory, shares deployment cycles, and shares failure modes. Splitting by domain means each service can be deployed, scaled, and debugged independently, which matters more in healthcare than in most other domains, since an outage in payment processing is a very different severity of problem than an outage in symptom checking.
Every service is built on Spring Boot 3 and Java 17, which keeps the runtime environment consistent across the backend even though each service is functionally isolated. Consistency at the runtime layer while keeping isolation at the domain layer is a deliberate middle ground. It avoids the operational cost of running eight completely different tech stacks while still getting the isolation benefits of separate services.
Why Domain Boundaries Instead of Technical Layers
A common mistake when splitting a system into services is slicing by technical concern instead of by business domain, for example a separate service for database access and another for business logic. That approach tends to reintroduce tight coupling immediately, since almost every request still has to cross both services to do anything useful.
MediCare avoids this by drawing boundaries around real world entities in a healthcare workflow. The Patient service owns everything about patient records. The Doctor service owns everything about doctor availability and credentials. Appointment sits between the two, since booking a slot is inherently a coordination problem between a patient and a doctor's schedule, not something that naturally belongs entirely inside either service. That decision, to give appointment booking its own service rather than folding it into either patient or doctor, was one of the more deliberate calls in the design, because appointment state has to stay consistent even as patient and doctor data changes independently underneath it.
The Appointment Service and the State Synchronization Problem
Of all eight services, appointment carries the most coordination complexity. A booking touches at least three other domains: it needs a valid patient, a doctor whose availability actually allows the requested slot, and eventually a payment record once the appointment is confirmed. None of those three things live inside the appointment service itself.
This is the same problem every microservice system eventually runs into, which is that business transactions rarely respect service boundaries cleanly. The appointment service handles this by treating booking as a sequence of state transitions rather than a single atomic operation. A booking moves from requested, to confirmed, to either completed or cancelled, and each transition is what gets synchronized rather than the entire record at once. This keeps the blast radius of any single failure smaller. If the payment step fails after a slot is tentatively held, the appointment can fall back to a cancelled state without leaving the doctor's calendar incorrectly locked.
Telemedicine as Its Own Domain
Splitting telemedicine out from appointment management, rather than treating a video consultation as just another property of an appointment record, was a deliberate separation. Video session management has an entirely different operational profile than booking logic. It deals with connection state, session duration, and third party video infrastructure, none of which has anything to do with how a slot gets reserved on a calendar.
Keeping telemedicine as an independent service means that if video infrastructure has a bad day, whether that is a provider outage or a spike in concurrent sessions, appointment booking for in person visits keeps working without any dependency on the health of the video layer. That kind of isolation is exactly the payoff that justifies the added operational complexity of running eight services instead of one.
AI Symptom Checking as an Isolated, Replaceable Dependency
The AI Symptom service wraps a third party AI integration for symptom diagnostics behind its own internal API. This is worth calling out specifically because it is the one service in the system most likely to change its underlying provider over time, whether due to cost, accuracy, or availability of a better model.
By keeping it isolated behind its own service boundary, swapping the underlying AI provider becomes a change contained entirely to one service, with no ripple effect into patient records, appointments, or any other part of the system. This is the same reasoning that shows up anywhere an external, evolving dependency is kept behind a stable internal boundary, so the rest of the system does not need to change when the dependency does.
The Role of the API Gateway
Every one of those eight services sits behind a single NGINX gateway on port 8080. This is not just a routing convenience. The gateway is doing real work at the edge of the system:
Centralizing these concerns at the gateway instead of repeating them inside every microservice matters for a simple reason. Security headers and rate limiting are the kind of thing that get forgotten in at least one service if every team implements them independently. Putting that responsibility in one place means the entire system inherits the same baseline protection, regardless of which service handles a given request.
Authentication Across Eight Independent Services
JWT issuance lives entirely in the Auth service, but every other service needs to validate that token on incoming requests without calling back to Auth on every single request, since that would turn one central service into a bottleneck and a single point of failure for the entire platform.
The practical answer is shared JWT filter logic, actively maintained in the auth service and reused by the other services, rather than reimplemented independently in each one. This is one of the sharper tensions in a microservices system, since keeping services independent is the whole point, but authentication is one of the few concerns that genuinely benefits from being consistent everywhere. The tradeoff accepted here is a small, deliberate coupling on shared filter logic in exchange for avoiding either a synchronous dependency on the Auth service for every request, or worse, eight slightly different implementations of JWT validation that drift out of sync over time.
Deployment: Kubernetes First, Docker Compose as a Fallback
MediCare ships with a one click PowerShell setup script that provisions Minikube, downloads missing tooling like kubectl, and deploys the full platform automatically. The project also supports a plain Docker Compose path for anyone who wants to run the system without a Kubernetes cluster.
Having both options is intentional rather than redundant. Kubernetes is the actual target environment, since it is what gives the system rolling deployments, service level scaling, and the kind of orchestration a multi service healthcare platform eventually needs in production. But requiring Kubernetes knowledge just to run the project locally raises the barrier to entry significantly. Docker Compose exists specifically to let someone spin up the whole backend with a single command while still working against the real microservice boundaries, rather than a simplified local only version of the system.
Running on Minikube locally also means the deployment manifests used in development are structurally the same kind of artifact used in a real cluster, rather than a separate local configuration that diverges from production over time. That matters because manifest drift between local and production environments is one of the more common sources of "it worked on my machine" failures in Kubernetes based systems.
Frontend: A Single Page Application Talking to One Gateway
The web client is a React single page application built with Vite and styled with Tailwind v4, using a shadcn inspired component approach. Architecturally, the frontend's job is simple by design. It never talks to any of the eight backend services directly. Every request goes through the same NGINX gateway that the rest of the system uses, which means the frontend does not need to track eight different service URLs or handle inconsistent auth logic per service. One gateway, one contract, regardless of how many services sit behind it.
This also means the frontend layer of work can proceed largely independent of backend service boundaries. As long as the gateway's contract stays stable, individual services can be refactored, rescaled, or even rewritten without the frontend needing to know.
Secrets Management Across Services
Healthcare data means credentials matter more than usual. MongoDB connection strings are read from environment variables, primarily SPRING_DATA_MONGODB_URI, with service specific fallback keys like MONGO_URI_PAYMENT supported for cases where Kubernetes secrets need to be scoped per service rather than shared globally.
For CI/CD, Jenkins pulls credentials from a secret file credential matching a checked in template, so the actual values, MongoDB URIs, Stripe keys, OpenAI keys, and JWT secrets, are never present in the repository itself. This is a standard pattern, but it is worth calling out because the alternative, a single shared .env committed at some point in a project's early history, is a common and serious mistake in projects handling anything resembling patient data. The explicit instruction in the project to rotate any credential that gets accidentally pushed reflects that this is treated as a real operational risk, not a checkbox.
Scoping secrets per service, rather than injecting one shared credential bundle into every pod, also limits what a compromised service can actually access. If the notification service were ever compromised, it has no legitimate reason to hold a payment database credential, so it should not have one available to it in the first place. That is a direct extension of the same isolation principle that justified splitting the backend into eight services in the first place, applied down at the credential level instead of the code level.
Continuous Integration
Every push to main triggers a GitHub Actions pipeline that builds and pushes each microservice image to GitHub Container Registry, then validates the Kubernetes manifests in the k8s/ directory before anything is considered deployable. Validating manifests as part of CI, rather than only at deploy time, catches configuration drift early, which matters more here than in a single service project, since a broken manifest for one service can silently prevent that one piece of a multi service system from rolling out while everything else continues running normally.
Building and pushing eight separate images per push, rather than one combined image, is a direct consequence of the service isolation decision made earlier in the architecture. It costs more in CI runtime and registry storage, but it means a change to the notification service produces a new notification image only, without forcing a rebuild or a version bump across services that were never touched.
The Cost Side of This Architecture
It is worth being direct about what this design gives up, since every architectural decision here trades something for something else.
Debugging a request that touches Auth, Patient, Doctor, and Appointment in sequence means correlating logs across four separate services instead of reading one stack trace in one process. Running eight services locally also means eight separate MongoDB connections, eight sets of environment variables, and eight processes to keep track of during development, even before Kubernetes enters the picture. None of that is free, and a simpler booking application for a single clinic would not justify paying for it.
The eight way split only earns its complexity because of what the system actually has to guarantee: that a failure in one domain, whether that is a video provider outage in telemedicine or a payment gateway issue, does not take down unrelated parts of a system that is handling real patient data. That guarantee is the entire justification for the added operational overhead, and it is worth stating plainly rather than assuming the architecture is self evidently correct.
What This Architecture Actually Buys
The value of this design is not that microservices are inherently better than a monolith. It is that the specific failure isolation, independent scaling, and centralized security enforcement line up with what a healthcare platform actually needs. Appointment booking staying available during a billing service issue, or symptom checking scaling independently from video consultation load, are the kind of properties this architecture is built to guarantee.
Getting Started
# Fastest path: one click setup
.\setup-medicare.ps1
# With a local gateway tunnel on Windows + Minikube
.\setup-medicare.ps1 -PortForwardGateway# Frontend
cd web-client
npm install
npm run devThe live deployment is running at medicarelk.duckdns.org, and the k8s/ directory is the clearest starting point for understanding how the eight services are wired together in production.


