All articles
Healthcare11 min read2024-10-28

Designing EMR Microservices with FHIR R4 Backends and Angular 17 Micro-Frontends

The architecture behind Octdaily's modular EMR platform — independently deployable .NET 8 FHIR services paired with Angular 17 micro-frontends, federated into a single shell app.

MicroservicesFHIR R4.NET 8Angular 17Micro-FrontendEMR

Why Microservices for an EMR?

A monolithic EMR is a liability. When the Medication module has a bug, the entire system goes down. When Patient Demographics needs a schema change, every team is blocked. When one team moves fast, every other team suffers.

At Octdaily, we designed the EMR as a collection of independently deployable services — each owning a FHIR resource domain, each with its own .NET 8 backend and Angular 17 micro-frontend.

Service Map

| Service | FHIR Resources | Port | Team | |---------|---------------|------|------| | Patient Demographics | Patient, RelatedPerson | 5001 | Team A | | Clinical Notes | DocumentReference, Composition | 5002 | Team B | | Medications | MedicationRequest, MedicationAdministration | 5003 | Team C | | Lab Results | DiagnosticReport, Observation | 5004 | Team D | | Care Coordination | CarePlan, CareTeam, Goal | 5005 | Team E | | ADT Events | Encounter, Location | 5006 | Team F | | Billing | Claim, ExplanationOfBenefit | 5007 | Team G |

Backend: FHIR-Native .NET 8 Services

Each service is a .NET 8 Minimal API that proxies and enriches the Azure FHIR server:

// PatientDemographicsService/Program.cs
var builder = WebApplication.CreateBuilder(args);
 
builder.Services
    .AddFhirClient(builder.Configuration["FhirServerUrl"])
    .AddSmartAuthentication()
    .AddAuditLogging()
    .AddHipaaCompliance();
 
var app = builder.Build();
 
app.MapGet("/api/patients/{id}", async (
    string id,
    IFhirPatientService svc,
    CancellationToken ct) =>
{
    var patient = await svc.GetByIdAsync(id, ct);
    return patient is null ? Results.NotFound() : Results.Ok(patient);
})
.RequireAuthorization("patient.read")
.WithName("GetPatient")
.WithOpenApi();
 
// FHIR $everything operation — full patient summary
app.MapGet("/api/patients/{id}/$everything", async (
    string id,
    IFhirPatientService svc,
    CancellationToken ct) =>
{
    var bundle = await svc.GetEverythingAsync(id, ct);
    return Results.Ok(bundle);
})
.RequireAuthorization("patient.read");

Frontend: Angular 17 Module Federation

Each service ships an Angular 17 micro-frontend using Webpack Module Federation:

// patient-demographics/webpack.config.ts
module.exports = withModuleFederationPlugin({
  name: 'patientDemographics',
  exposes: {
    './PatientModule': './src/app/patient/patient.module.ts',
    './PatientRoutes': './src/app/patient/patient.routes.ts',
  },
  shared: share({
    '@angular/core': { singleton: true, strictVersion: true },
    '@angular/router': { singleton: true, strictVersion: true },
    '@fhir-client/angular': { singleton: true },
  }),
})

The shell app (Angular 17) dynamically loads each micro-frontend at runtime:

// shell/src/app/app.routes.ts
export const routes: Routes = [
  {
    path: 'patients',
    loadChildren: () =>
      loadRemoteModule({
        remoteEntry: environment.patientDemographicsUrl + '/remoteEntry.js',
        remoteName: 'patientDemographics',
        exposedModule: './PatientModule',
      }).then((m) => m.PatientModule),
  },
  {
    path: 'medications',
    loadChildren: () =>
      loadRemoteModule({
        remoteEntry: environment.medicationsUrl + '/remoteEntry.js',
        remoteName: 'medications',
        exposedModule: './MedicationsModule',
      }).then((m) => m.MedicationsModule),
  },
]

Cross-Service Patient Context

All micro-frontends share patient context via a lightweight NgRx store in the shell:

// shell/src/app/store/patient-context.store.ts
export const PatientContextStore = signalStore(
  { providedIn: 'root' },
  withState<PatientContext>({ patientId: null, encounterId: null }),
  withMethods((store) => ({
    setPatient: (patientId: string, encounterId?: string) =>
      patchState(store, { patientId, encounterId }),
  }))
)

When a clinician opens a patient chart, all loaded micro-frontends immediately know which patient is in context.

Deployment on AKS

Each service deploys independently on Azure Kubernetes Service:

# patient-demographics/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: patient-demographics
  labels:
    app: patient-demographics
    fhir-domain: patient
spec:
  replicas: 3
  selector:
    matchLabels:
      app: patient-demographics
  template:
    spec:
      containers:
        - name: api
          image: octdaily.azurecr.io/patient-demographics:latest
          env:
            - name: FHIR_SERVER_URL
              valueFrom:
                secretKeyRef:
                  name: fhir-secrets
                  key: server-url

Outcomes

  • 7 independent teams ship without blocking each other
  • Zero-downtime deployments — each service rolls out independently
  • Medication module can update 3x per day; Clinical Notes once per week — each on its own cadence
  • New SNF onboarded by adding one FHIR data adapter, no frontend changes needed