Web Development

Beyond the Monolith: A Health Tech Guide to .NET Microservices

A .NET developer's guide to microservices for health tech. Learn when to break a monolith for clinical safety and how to build your first service with Docker & YARP.

10 November 2025
9 min read

For decades, the "monolith" has been the default for many clinical applications. One large codebase, one large database, one deployment. It's a single, unified system. It's simple to get started with, and for a long time, it was the most reliable choice.

But what happens when that simple departmental system grows into a beast?

What happens when your Patient Administration System (PAS) also needs to handle e-Prescribing (ePMA), Bed Management, and Outpatient Appointments? Your team grows from 3 to 30. A bug in the Bed Management module (which is non-critical) causes a memory leak that crashes the e-Prescribing service during a ward round. You need to scale the "Appointments API" for a new patient-facing app, but you're forced to regression test and deploy the entire 2GB clinical application to do it.

You're no longer building a simple app. You're fighting a monolith.

This is the moment when "microservices" stops being a buzzword and becomes a clinical safety and scalability necessity. This guide will walk you through what they are, when to use them in a healthcare context, and how to set up both your first service and the "front door" that makes them all work together.


What Are Microservices?

A microservice architecture is a style that structures an application as a collection of small, independent, and loosely coupled services, each responsible for a single business capability.

Let's break that down with a clinical analogy:

  • The Monolith: Imagine a single, large hospital building trying to do everything. The same reception desk has to check in A&E patients, outpatient appointments, and visiting families. The same IT system runs pharmacy, radiology, and patient records. If the power in the radiology wing fails, it could knock out the power to the entire hospital, including critical care.
  • Microservices: Imagine a hospital campus. You have a dedicated A&E building, a separate Outpatient Clinic, a dedicated Pharmacy, and a Radiology building. Each is a specialist unit with its own staff, resources, and often its own lightweight, specific IT system. If the Outpatient Clinic's admin system goes down, it's a problem, but A&E, Pharmacy, and e-Prescribing continue to run perfectly.

In .NET health tech terms: | Feature | Monolith | Microservices | | :--- | :--- | :--- | | Codebase | One large sln file containing all modules (PAS, ePMA, Orders, etc.). | Separate sln files for PatientService, OrderCommsService, AuthService. | | Database | One massive SQL database with hundreds of tables, shared by all modules. | Each service owns its own data. PatientService has its Patient table. PrescribingService has its Medications table. | | Deployment| You must test and deploy the entire application as one unit. | You can deploy an update to the PrescribingService without touching or re-testing the PatientService. | | Faults | A bug in the (non-critical) Dietary module can crash the (critical) MedicationAdministration module. | A crash in the DietaryService is isolated. Medication administration is unaffected. |


When Should You Actually Use Microservices?

Here's the most important advice you will ever get on this topic: Don't start with microservices.

It's a principle we call YAGNI ("You Ain't Gonna Need It"). Starting a new "ward dashboard" app as 10 microservices is a massive "complexity tax." You will spend more time on DevOps, networking, and inter-service communication than on building the clinical workflow.

A well-structured monolith is the right choice for most projects. You should only "break the monolith" when you feel specific, identifiable pain points:

  1. Team Scaling Pain: Your development team is growing (e.g., > 10 developers), and they are constantly causing merge conflicts and blocking each other's deployments in the same large clinical system codebase.
  2. Independent Scaling Needs: One small part of your app gets 90% of the traffic (e.g., the PatientSearch API used by every clinician), and you're forced to scale the entire 5GB monolith (servers, RAM, etc.) just to handle that one feature.
  3. Technology Diversification: Your main app is in .NET, but you need to build a new DICOM image processing service (which uses C++ libraries) or a legacy HL7v2 integration engine (which uses a specific integration tool like Mirth Connect). Microservices allow you to build these specialist services using the best tool for the job.
  4. Fault Isolation & Clinical Safety: This is the big one in health tech. A bug in a non-critical module (like BedManagement) must not be able to crash a critical module (like e-Prescribing). By splitting them, you ensure the ePMA service is independently resilient.

If you aren't feeling at least two of these, stick with your monolith.


Part 1: How to Build Your First .NET Microservice

Let's say you've decided to break out your "Patient" data from your monolith. Your goal is to create a new, completely independent PatientService that acts as the single source of truth for patient demographics.

Step 1: Create the .NET Web API Project

This is the simplest part. Open your terminal and use the .NET CLI to create a new, minimal Web API project.

# Create a new solution folder
mkdir MaxJefferyTech.ClinicalServices
cd MaxJefferyTech.ClinicalServices

# Create a new Web API project
dotnet new webapi -n PatientService

You now have a new PatientService folder containing a lightweight .NET project.

Step 2: Define Your Service's Logic

This service will only do one thing. Let's add a simple controller to get patients. (This is a simplified example. In a real app, you'd connect to a dedicated patient database using Prisma or EF Core).

Controllers/PatientsController.cs

using Microsoft.AspNetCore.Mvc;

namespace PatientService.Controllers;

[ApiController]
[Route("api/[controller]")]
public class PatientsController : ControllerBase
{
// Mock data. In a real app, this comes from your service's private database.
private static readonly List<Patient> Patients = new()
{
new Patient { Id = 1, NhsNumber = "1234567890", Name = "Jane Doe" },
new Patient { Id = 2, NhsNumber = "0987654321", Name = "John Smith" }
};

[HttpGet("{id}")]
public IActionResult GetPatientById(int id)
{
var patient = Patients.FirstOrDefault(p => p.Id == id);
if (patient == null)
{
return NotFound();
}
return Ok(patient);
}

[HttpGet("ByNhsNumber/{nhsNumber}")]
public IActionResult GetPatientByNhsNumber(string nhsNumber)
{
var patient = Patients.FirstOrDefault(p => p.NhsNumber == nhsNumber);
if (patient == null)
{
return NotFound();
}
return Ok(patient);
}
}

public class Patient
{
public int Id { get; set; }
public string NhsNumber { get; set; }
public string Name { get; set; }
}

You can now run this service completely on its own: cd PatientService dotnet run

Your new PatientService is now running, completely independent, on http://localhost:5001 (or a similar port).

Step 3: Containerize Your Service with Docker

The real power of microservices comes from containerization. This packages your service (and its .NET runtime) into a single, portable unit.

Create a Dockerfile in your PatientService project's root folder:

# Stage 1: Build the app
FROM [mcr.microsoft.com/dotnet/sdk:8.0](https://mcr.microsoft.com/dotnet/sdk:8.0) AS build
WORKDIR /src
COPY . .
RUN dotnet restore "PatientService/PatientService.csproj"
RUN dotnet publish "PatientService/PatientService.csproj" -c Release -o /app/publish

# Stage 2: Create the final, lightweight image
FROM [mcr.microsoft.com/dotnet/aspnet:8.0](https://mcr.microsoft.com/dotnet/aspnet:8.0) AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "PatientService.dll"]

You can now build and run this service anywhere: docker build -t patientservice:latest . docker run -p 8080:8080 patientservice:latest


Part 2: The Next Step (Making Services Talk to Each Other)

You now have one independent service. But soon you'll have three:

  • PatientService (on port 8080)
  • AppointmentService (on port 8081)
  • PrescribingService (on port 8082)

How does your frontend (or another service) know which one to talk to? This is the problem a Reverse Proxy (or API Gateway) solves.

A Reverse Proxy is a single, smart "front door" for your entire backend. Your frontend doesn't know about your 10 microservices. It only knows about one URL: https://api.maxjeffery.tech.

The proxy acts as the traffic cop. It inspects the incoming request and forwards it to the correct internal service.

  • https://api.maxjeffery.tech/patients/123 -> http://localhost:8080/api/patients/123
  • https://api.maxjeffery.tech/appointments/all -> http://localhost:8081/api/appointments/all

This is powerful because the proxy can centralize all your cross-cutting concerns:

  1. SSL Termination: It handles the single HTTPS certificate.
  2. Authentication: It can validate the user's JWT token once.
  3. Load Balancing: If you have 5 copies of your PrescribingService running, the proxy will distribute the traffic.
  4. Routing: It maps your clean, public URL structure to your internal service addresses.


How to Build a .NET Reverse Proxy with YARP

You could use Nginx, but Microsoft has a first-class, open-source tool called YARP: Yet Another Reverse Proxy.

The beauty of YARP is that it's just a .NET library. You build your proxy as a standard, lightweight ASP.NET Core application.

Step 1: Create a new ASP.NET "Empty" Project

dotnet new web -n ApiGateway

Step 2: Install YARP

cd ApiGateway dotnet add package Yarp.ReverseProxy

Step 3: Configure Program.cs

It's just two lines of code.

// In ApiGateway/Program.cs
var builder = WebApplication.CreateBuilder(args);

// 1. Load the reverse proxy config from appsettings.json
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();

// 2. Map all incoming requests to the proxy
app.MapReverseProxy();

app.Run();

Step 4: Configure appsettings.json

This is your "rulebook." You define your "Routes" (what you listen for) and your "Clusters" (where you send the traffic).

{
"ReverseProxy": {
"Routes": {
"patients-route": {
"ClusterId": "patients-cluster",
"Match": {
"Path": "/patients-api/{**catch-all}"
},
"Transforms": [
{ "PathPattern": "/api/patients/{**catch-all}" }
]
},
"appointments-route": {
"ClusterId": "appointments-cluster",
"Match": {
"Path": "/appointments-api/{**catch-all}"
},
"Transforms": [
{ "PathPattern": "/api/appointments/{**catch-all}" }
]
}
},
"Clusters": {
"patients-cluster": {
"Destinations": {
"destination1": {
"Address": "http://localhost:8080"
}
}
},
"appointments-cluster": {
"Destinations": {
"destination1": {
"Address": "http://localhost:8081"
}
}
}
}
}
}

And just like that, you have a high-performance, .NET-native reverse proxy. When your ApiGateway receives a request to /patients-api/123, it will forward it to http://localhost:8080/api/patients/123.

Conclusion

Don't start with microservices. Start with a well-structured monolith.

But when your monolith's "growing pains" become "clinical safety" or "scaling" pains, you have a clear path forward. By breaking out a single clinical capability (like Patient Demographics) into a dedicated .NET microservice, you can deploy it, scale it, and manage it independently, building a more resilient and maintainable clinical system.

Struggling to scale your .NET clinical application? As a specialist in .NET architecture with experience in clinical systems, I help businesses migrate from legacy monoliths to modern, high-performance, and maintainable systems.

Found this helpful?

Get in touch to discuss your project or learn more about my development services.