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.
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-urlOutcomes
- 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