How to Design a Telemedicine & Virtual Health Platform
End-to-End Architecture for Video Consultations, EHR Integration, E-Prescribing, and HIPAA-Compliant Virtual Care at Scale
1. Introduction & The Rise of Telehealth
Telemedicine has evolved from a niche convenience to a critical pillar of modern healthcare delivery. The COVID-19 pandemic compressed a decade of digital health adoption into months, and the structural shift has proven permanent. By 2026, telehealth accounts for roughly 20-25% of all outpatient visits in the United States, and the global virtual care market is projected to exceed billion.
Designing a telemedicine platform is among the most complex system design challenges in existence. It sits at the intersection of real-time communication (video, audio, screen sharing), regulatory compliance (HIPAA, state licensing, DEA regulations), clinical workflows (EHR integration, prescription management, lab orders), financial operations (insurance verification, billing, CPT/ICD-10 coding), and consumer-grade user experience (zero-friction scheduling, waiting rooms, mobile-first design).
A typical virtual visit involves over 15 distinct microservices coordinating in real-time. Unlike a social media or e-commerce platform, a telehealth system cannot tolerate eventual consistency in critical paths — a dropped video call during an urgent consultation, a lost prescription, or a breached patient record carries clinical and legal consequences that go far beyond lost revenue.
In this deep dive, we will build a complete telemedicine platform from the ground up — covering every major subsystem from video infrastructure and appointment scheduling to EHR integration, e-prescribing, insurance verification, vitals monitoring, AI-assisted triage, and HIPAA-compliant security architecture. We will use C# for backend services, Mermaid for architecture diagrams, and real-world patterns drawn from platforms like Teladoc, Amwell, MDLive, and Doxy.me.
Key Design Challenges
- Real-time video with sub-200ms latency — WebRTC signaling, TURN/STUN infrastructure, adaptive bitrate, and global edge routing
- HIPAA compliance at every layer — end-to-end encryption, BAA agreements, audit logging, minimum necessary access, and breach notification workflows
- EHR interoperability — HL7 FHIR R4 integration, SMART on FHIR authentication, terminology services (SNOMED CT, ICD-10, RxNorm)
- Real-time state management — provider availability, waiting room queues, call status, and vitals streams must be consistent and low-latency
- Multi-stakeholder workflows — patients, providers, pharmacists, lab technicians, insurance processors, and administrators all interact with different views of the same data
- Graceful degradation — audio-only fallback for video failures, offline clinical note caching, retry logic for insurance APIs
2. Functional & Non-Functional Requirements
Functional Requirements
| Module | Features | Priority |
|---|---|---|
| Video Consultation | 1:1 HD video/audio, screen sharing, adaptive bitrate, audio-only fallback | P0 |
| Appointment Scheduling | Provider availability, patient booking, recurring appointments, reminders, timezone handling | P0 |
| Waiting Room | Virtual check-in, queue position, estimated wait time, provider notifications | P0 |
| Provider Directory | Search by specialty, availability, insurance, language, rating; provider profiles | P0 |
| EHR Integration | FHIR R4 read/write, patient demographics, clinical documents, allergies, medications | P0 |
| Prescriptions | E-prescribing via NCPDP SCRIPT, pharmacy routing, prior auth workflows | P0 |
| Lab Orders & Results | Lab order creation, result delivery, abnormal result flagging, provider review | P1 |
| Insurance Verification | Real-time eligibility checks, copay estimation, prior authorization | P1 |
| Payment Processing | Copay collection, split billing, superbills, sliding scale fees | P0 |
| Secure Messaging | Async chat, file attachment (images, PDFs), read receipts, provider queue | P1 |
| Multi-Party Calls | 3+ participants, specialist referral mid-call, interpreter support | P1 |
| Recording | Session recording with explicit consent, secure storage, playback | P2 |
| Clinical Notes | SOAP note templates, auto-save, ICD-10/CPT coding suggestions, sign-off workflow | P0 |
| Billing & Coding | CPT/ICD-10 code management, claim generation, ERA/835 processing | P1 |
| Vitals Monitoring | Connected device data ingestion (BP, SpO2, glucose), real-time display, alerting | P2 |
| AI Triage | Symptom checker, acuity scoring, routing logic, escalation workflows | P2 |
| Consent Management | Informed consent capture, e-signatures, consent versioning, audit trail | P0 |
| Provider Credentialing | License verification, DEA registration, board certification, privileging | P1 |
Non-Functional Requirements
| Requirement | Target | Notes |
|---|---|---|
| Availability | 99.95% (video 99.99%) | Multi-region active-active for video |
| Video Latency | < 200ms end-to-end | WebRTC with edge TURN servers |
| API Latency | p99 < 500ms | Scheduling, directory, clinical APIs |
| Concurrent Users | 100K video sessions | Scalable media servers (SFU) |
| Encryption | E2E video, AES-256 rest, TLS 1.3 transit | HIPAA-mandatory standards |
| Audit Logging | 100% PHI access events | Immutable log, 7-year retention |
| Disaster Recovery | RPO < 1 min, RTO < 15 min | Hot standby, automatic failover |
| Compliance | HIPAA, SOC 2 Type II, HITRUST | Annual audits, pen testing |
3. High-Level Architecture Overview
Architecture Principles
- Event-Driven Core: Kafka serves as the central nervous system. Every state change — appointment booked, vitals received, prescription sent, consent captured — emits an event. Downstream services subscribe to relevant topics for loose coupling.
- Bounded Contexts: Each service owns its data store and business logic. No shared databases. Cross-service queries go through APIs or materialized views built from event streams.
- Defense in Depth: WAF at the edge, mTLS between services, RBAC + ABAC within services, field-level encryption for PHI columns, audit logging at every access point.
- Graceful Degradation: If video quality degrades, fall back to audio-only. If insurance verification times out, allow self-pay. If FHIR integration is down, use cached patient summaries.
Service Communication Patterns
| Pattern | Use Cases | Implementation |
|---|---|---|
| Synchronous REST/gRPC | API queries, authentication, lookups | gRPC internal, REST external |
| Async Events (Kafka) | Clinical events, billing, audit | Avro schema, exactly-once |
| WebSocket / SSE | Real-time vitals, waiting room, chat | Redis pub/sub fan-out |
| WebRTC Data Channels | In-call signaling, file transfer | Custom signaling over WSS |
4. Data Model & Storage Schema
The data model spans multiple storage systems because different data types have fundamentally different access patterns. Patient demographics are relational. Clinical notes are document-oriented. Recordings are binary blobs. Vitals are time-series.
PostgreSQL — Core Entities
SQL
CREATE TABLE patients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
external_id VARCHAR(64) UNIQUE,
first_name VARCHAR(128) NOT NULL,
last_name VARCHAR(128) NOT NULL,
date_of_birth DATE NOT NULL,
email VARCHAR(255) UNIQUE,
phone VARCHAR(20),
address_json JSONB,
insurance_json JSONB,
emergency_contact JSONB,
preferred_language VARCHAR(10) DEFAULT 'en',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
npi_number VARCHAR(10) UNIQUE NOT NULL,
specialty VARCHAR(100) NOT NULL,
sub_specialties TEXT[],
license_state VARCHAR(2) NOT NULL,
license_number VARCHAR(32) NOT NULL,
license_expiry DATE NOT NULL,
dea_number VARCHAR(20),
board_certified BOOLEAN DEFAULT false,
credentialing_status VARCHAR(20) DEFAULT 'pending',
accepting_patients BOOLEAN DEFAULT true,
max_daily_visits INT DEFAULT 20,
visit_duration_min INT DEFAULT 30,
video_enabled BOOLEAN DEFAULT true,
rating_avg DECIMAL(3,2),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE appointments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID REFERENCES patients(id) NOT NULL,
provider_id UUID REFERENCES providers(id) NOT NULL,
scheduled_at TIMESTAMPTZ NOT NULL,
duration_min INT NOT NULL DEFAULT 30,
status VARCHAR(20) NOT NULL DEFAULT 'scheduled',
visit_type VARCHAR(30) NOT NULL,
chief_complaint TEXT,
insurance_verified BOOLEAN DEFAULT false,
copay_amount DECIMAL(10,2),
payment_status VARCHAR(20) DEFAULT 'pending',
room_id UUID,
actual_start TIMESTAMPTZ,
actual_end TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE provider_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id UUID REFERENCES providers(id) NOT NULL,
day_of_week INT NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
slot_duration_min INT DEFAULT 30,
timezone VARCHAR(50) NOT NULL,
is_active BOOLEAN DEFAULT true,
effective_from DATE DEFAULT CURRENT_DATE,
effective_to DATE
);
CREATE TABLE appointment_slots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id UUID REFERENCES providers(id) NOT NULL,
slot_start TIMESTAMPTZ NOT NULL,
slot_end TIMESTAMPTZ NOT NULL,
status VARCHAR(20) DEFAULT 'available',
held_by UUID,
held_until TIMESTAMPTZ,
appointment_id UUID REFERENCES appointments(id)
);
CREATE INDEX idx_slots_provider_time
ON appointment_slots(provider_id, slot_start)
WHERE status = 'available';
CREATE TABLE consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID REFERENCES patients(id) NOT NULL,
consent_type VARCHAR(50) NOT NULL,
version VARCHAR(10) NOT NULL,
content_hash VARCHAR(64) NOT NULL,
signed_at TIMESTAMPTZ,
ip_address INET,
expires_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
event_time TIMESTAMPTZ DEFAULT NOW(),
actor_id UUID NOT NULL,
actor_type VARCHAR(20) NOT NULL,
action VARCHAR(50) NOT NULL,
resource_type VARCHAR(50) NOT NULL,
resource_id UUID,
patient_id UUID,
ip_address INET,
details JSONB,
session_id UUID
);
CREATE INDEX idx_audit_time ON audit_log(event_time);
CREATE INDEX idx_audit_patient ON audit_log(patient_id);
MongoDB — Clinical Notes
JSON
{
"_id": ObjectId("..."),
"appointmentId": UUID("..."),
"patientId": UUID("..."),
"providerId": UUID("..."),
"noteType": "soap",
"status": "draft",
"subjective": {
"chiefComplaint": "Persistent headache for 5 days",
"historyOfPresentIllness": "Patient reports bilateral frontal headache...",
"reviewOfSystems": {
"neurological": "No visual changes, no numbness",
"constitutional": "Low-grade fever, fatigue"
}
},
"objective": {
"vitals": { "bloodPressure": "128/82", "heartRate": 88, "temperature": 99.2 },
"examination": "Alert, oriented. Cranial nerves II-XII intact."
},
"assessment": [
{ "diagnosis": "Tension-type headache", "icd10Code": "G44.209", "confidence": 0.85, "isPrimary": true }
],
"plan": {
"medications": ["Acetaminophen 500mg PO Q6H PRN"],
"instructions": "Rest, hydration, follow up if no improvement in 7 days",
"followUp": "2 weeks or sooner if symptoms worsen"
},
"cptCodes": ["99213"],
"signatures": { "provider": { "signedAt": ISODate("2026-07-10T14:32:00Z") } },
"version": 1,
"createdAt": ISODate("2026-07-10T14:15:00Z")
}
Redis — Session & Real-Time State Keys
C#
public static class RedisKeys
{
public static string WaitingRoom(Guid providerId) =>
$"tele:waitingroom:{providerId}";
public static string ProviderStatus(Guid providerId) =>
$"tele:provider:status:{providerId}";
public static string VideoSession(Guid sessionId) =>
$"tele:video:session:{sessionId}";
public static string ProviderSlots(Guid providerId, DateTime date) =>
$"tele:slots:{providerId}:{date:yyyy-MM-dd}";
public static string RateLimit(Guid patientId) =>
$"tele:ratelimit:{patientId}";
public static string AppointmentHold(Guid slotId) =>
$"tele:hold:slot:{slotId}";
}
5. Video Consultation Engine (WebRTC)
The video consultation system is the heart of any telemedicine platform. Users expect crystal-clear, low-latency video that works on any device. WebRTC is powerful but notoriously complex to operate at scale.
WebRTC Architecture: SFU Model
We use a Selective Forwarding Unit (SFU) architecture. In a mesh model, every participant sends a stream to every other participant, creating N*(N-1) connections. This works for 2-3 participants but collapses beyond that. An SFU receives all publisher streams and selectively forwards them to each subscriber.
Signaling Server Implementation
C#
[Authorize]
public class VideoSignalingHub : Hub
{
private readonly ISessionStore _sessionStore;
private readonly IAuditLogger _auditLogger;
private readonly ITurnService _turnService;
public VideoSignalingHub(
ISessionStore sessionStore,
IAuditLogger auditLogger,
ITurnService turnService)
{
_sessionStore = sessionStore;
_auditLogger = auditLogger;
_turnService = turnService;
}
public async Task JoinSession(Guid sessionId)
{
var session = await _sessionStore.GetSessionAsync(sessionId);
if (session == null)
throw new HubException("Session not found");
var userId = Context.UserIdentifier;
var role = session.PatientId.ToString() == userId
? ParticipantRole.Patient
: ParticipantRole.Provider;
await Groups.AddToGroupAsync(
Context.ConnectionId, sessionId.ToString());
var turnCredentials = await _turnService
.GetTemporaryCredentialsAsync(sessionId);
await Clients.Caller.SendAsync("TurnCredentials",
turnCredentials);
await Clients.Caller.SendAsync("SessionJoined", new
{
SessionId = sessionId, Role = role,
ICEServers = turnCredentials.IceServers
});
await Clients.GroupExcept(
sessionId.ToString(), Context.ConnectionId)
.SendAsync("PeerJoined", new
{
ParticipantId = userId, Role = role
});
await _auditLogger.LogAsync(new AuditEvent
{
Action = "video.session.join",
ResourceId = sessionId,
Details = new { Role = role }
});
}
public async Task SendOffer(Guid sessionId, string sdp)
{
await Clients.GroupExcept(
sessionId.ToString(), Context.ConnectionId)
.SendAsync("ReceiveOffer", new
{
From = Context.UserIdentifier, SDP = sdp
});
}
public async Task SendAnswer(Guid sessionId, string sdp)
{
await Clients.GroupExcept(
sessionId.ToString(), Context.ConnectionId)
.SendAsync("ReceiveAnswer", new
{
From = Context.UserIdentifier, SDP = sdp
});
}
public async Task SendIceCandidate(
Guid sessionId, string candidate)
{
await Clients.GroupExcept(
sessionId.ToString(), Context.ConnectionId)
.SendAsync("ReceiveIceCandidate", new
{
From = Context.UserIdentifier,
Candidate = candidate
});
}
public async Task EndSession(Guid sessionId)
{
await Clients.Group(sessionId.ToString())
.SendAsync("SessionEnded", new
{
EndedBy = Context.UserIdentifier,
Timestamp = DateTime.UtcNow
});
await _sessionStore.EndSessionAsync(sessionId);
await _auditLogger.LogAsync(new AuditEvent
{
Action = "video.session.end",
ResourceId = sessionId
});
}
public override async Task OnDisconnectedAsync(
Exception? ex)
{
var sessions = await _sessionStore
.GetActiveSessionsForUserAsync(
Context.UserIdentifier);
foreach (var session in sessions)
{
await Clients.GroupExcept(
session.Id.ToString(),
Context.ConnectionId)
.SendAsync("PeerDisconnected", new
{
ParticipantId = Context.UserIdentifier,
Reason = ex != null ? "error" : "left"
});
}
await base.OnDisconnectedAsync(ex);
}
}
Adaptive Bitrate & Network Quality
C#
public class NetworkQualityMonitor
{
private readonly ISfuClient _sfuClient;
private readonly QualityThresholds _thresholds = new()
{
Excellent = new NetworkProfile(2500, 0.5, 10,
VideoResolution.HD1080, 30),
Good = new NetworkProfile(1000, 2.0, 30,
VideoResolution.HD720, 30),
Fair = new NetworkProfile(500, 5.0, 80,
VideoResolution.SD480, 24),
Poor = new NetworkProfile(200, 10.0, 150,
VideoResolution.SD360, 15),
Critical = new NetworkProfile(0, 100, 9999,
VideoResolution.AudioOnly, 0)
};
public async Task EvaluateAndAdjustAsync(
Guid sessionId, NetworkStats stats)
{
var profile = DetermineProfile(stats);
var currentProfile =
await GetCurrentProfileAsync(sessionId);
if (profile != currentProfile)
{
await _sfuClient.AdjustVideoQualityAsync(
sessionId, profile);
}
}
private QualityLevel DetermineProfile(
NetworkStats stats)
{
if (stats.PacketLossPercent > 10.0
|| stats.JitterMs > 150)
return QualityLevel.Critical;
if (stats.AvailableBandwidthKbps >= 2500
&& stats.PacketLossPercent <= 0.5)
return QualityLevel.Excellent;
if (stats.AvailableBandwidthKbps >= 1000)
return QualityLevel.Good;
if (stats.AvailableBandwidthKbps >= 500)
return QualityLevel.Fair;
return QualityLevel.Poor;
}
}
Graceful Degradation: Audio-Only Fallback
| Condition | Action | User Experience |
|---|---|---|
| Bandwidth > 2.5 Mbps, loss < 0.5% | HD 1080p @ 30fps | Full HD video |
| Bandwidth 1-2.5 Mbps, loss < 2% | 720p @ 30fps | Clear HD video |
| Bandwidth 0.5-1 Mbps, loss < 5% | 480p @ 24fps | Standard quality |
| Bandwidth 0.2-0.5 Mbps, loss < 10% | 360p @ 15fps | Lower quality |
| Bandwidth < 0.2 Mbps | Audio only | Audio call with photo |
| Total connection failure | PSTN dial-in | Phone call fallback |
The dial-in fallback uses Twilio Programmable Voice. If both participants lose internet, they continue by phone. This is critical for urgent visits where a dropped connection could have clinical consequences.
6. Virtual Waiting Room & Queue Management
The virtual waiting room displays queue position, estimated wait time, and provides check-in functionality including consent forms, insurance card upload, and chief complaint entry.
Waiting Room Service
C#
public class WaitingRoomService : IWaitingRoomService
{
private readonly IDatabase _redis;
private readonly IAppointmentRepository _appointments;
public async Task<CheckInResult> CheckInAsync(
Guid appointmentId, CheckInRequest request)
{
var appointment = await _appointments
.GetByIdAsync(appointmentId);
if (appointment == null)
throw new NotFoundException(
"Appointment not found");
if (appointment.Status
!= AppointmentStatus.Scheduled)
throw new ConflictException(
$"Cannot check in: {appointment.Status}");
var checkIn = new WaitingRoomEntry
{
AppointmentId = appointmentId,
PatientId = appointment.PatientId,
ProviderId = appointment.ProviderId,
CheckedInAt = DateTime.UtcNow,
ChiefComplaint = request.ChiefComplaint,
ConsentGiven = request.ConsentGiven
};
await SaveCheckInAsync(checkIn);
var queueKey = RedisKeys.WaitingRoom(
appointment.ProviderId);
var score = DateTimeOffset.UtcNow
.ToUnixTimeMilliseconds();
await _redis.SortedSetAddAsync(
queueKey, appointmentId.ToString(), score);
var position = await _redis.SortedSetRankAsync(
queueKey, appointmentId.ToString());
var avgDuration =
await GetAverageVisitDurationAsync(
appointment.ProviderId);
var estimatedWait =
(position ?? 0) * avgDuration;
appointment.Status =
AppointmentStatus.CheckedIn;
await _appointments.UpdateAsync(appointment);
return new CheckInResult
{
Position = (int)(position ?? 0) + 1,
EstimatedWaitMinutes = estimatedWait
};
}
}
The waiting room uses Redis sorted sets for O(log N) position queries. The front-end receives real-time updates via SSE. When the provider clicks "Next Patient," the service pops the earliest check-in and transitions the patient to the video session.
7. Appointment Scheduling System
Scheduling handles provider availability across timezones, recurring schedules with exceptions, appointment holds to prevent double-booking, cancellation policies, and automated reminders.
Slot Generation Algorithm
C#
public class SlotGenerator
{
public async Task<List<TimeSlot>> GenerateSlotsAsync(
Guid providerId,
DateTime startDate, DateTime endDate)
{
var schedules = await GetActiveSchedulesAsync(
providerId, startDate, endDate);
var exceptions =
await GetScheduleExceptionsAsync(
providerId, startDate, endDate);
var bookings =
await GetBookedAppointmentsAsync(
providerId, startDate, endDate);
var blocked = await GetBlockedSlotsAsync(
providerId, startDate, endDate);
var slots = new List<TimeSlot>();
var currentDate = startDate.Date;
while (currentDate <= endDate.Date)
{
var daySchedule = schedules.FirstOrDefault(
s => s.DayOfWeek
== (int)currentDate.DayOfWeek);
if (daySchedule != null)
{
var fullDayOff = exceptions.Any(e =>
e.Date == currentDate
&& e.ExceptionType
== ScheduleExceptionType.FullDay);
if (!fullDayOff)
{
slots.AddRange(GenerateDaySlots(
currentDate, daySchedule,
exceptions, bookings, blocked));
}
}
currentDate = currentDate.AddDays(1);
}
return slots;
}
}
Appointment Reminder Pipeline
| Timing | Channel | Content |
|---|---|---|
| 24 hours before | SMS + Email | Appointment details, prep instructions |
| 2 hours before | SMS + Push | "Your visit is in 2 hours" |
| 15 minutes before | Push + Email | "Time to check in!" |
| At scheduled time | Push | "Your provider is ready" |
| No-show (15 min) | SMS | "We missed you. Reschedule" |
8. Provider Directory, Search & Matching
The provider directory is the front door. Patients search by specialty, condition, insurance, language, availability, and location.
C#
public class ProviderSearchService
{
private readonly IElasticClient _elastic;
private readonly IAvailabilityService _availability;
public async Task<SearchResult<ProviderResult>>
SearchAsync(ProviderSearchRequest request)
{
var esResponse = await _elastic
.SearchAsync<ProviderDocument>(s => s
.Index("providers")
.Query(q => q
.Bool(b => b
.Must(mu => mu
.MultiMatch(mm => mm
.Fields(f => f
.Field(p => p.Specialty, 3.0)
.Field(p => p.Conditions, 2.0)
.Field(p => p.Bio, 1.0))
.Query(request.QueryText)
.Fuzziness(Fuzziness.Auto)))
.Filter(f => f
.Term(t => t.Field(
p => p.AcceptingPatients, true))
&& f.Term(t => t.Field(
p => p.CredentialingStatus,
"active")))))
.From(request.Offset)
.Size(request.Limit));
var ids = esResponse.Documents
.Select(p => p.Id).ToList();
var avail = await _availability
.GetNextAvailableSlotsAsync(ids);
var results = esResponse.Documents
.Select(doc => new ProviderResult
{
Provider = doc,
NextAvailable =
avail.GetValueOrDefault(doc.Id),
Score = CalcScore(doc, avail, request)
})
.OrderByDescending(r => r.Score)
.ToList();
return new SearchResult<ProviderResult>
{
Items = results,
TotalCount = (int)esResponse.Total
};
}
}
Urgent Care Matching Factors
| Factor | Weight | Logic |
|---|---|---|
| Current availability | 35% | Provider online and free? |
| Triage acuity | 25% | Higher acuity to urgent provider |
| Specialty match | 20% | Exact specialty vs general |
| Patient history | 10% | Previously seen? |
| Language | 5% | Shared language |
| Load balancing | 5% | Distribute evenly |
9. EHR Integration (HL7 FHIR R4)
EHR integration is the most technically challenging aspect. Healthcare organizations use diverse EHR systems (Epic, Cerner, Allscripts, athenahealth), each with different APIs and authentication. HL7 FHIR R4 provides standardized RESTful APIs, but real-world adoption varies enormously.
FHIR Resource Architecture
FHIR Client Implementation
C#
public class FhirClient : IFhirClient
{
private readonly HttpClient _httpClient;
private readonly IFhirAuthenticator _auth;
private readonly IFhirSerializer _serializer;
public async Task<Patient> GetPatientAsync(
string patientId, FhirServerConfig server)
{
var token = await _auth.GetAccessTokenAsync(
server, FhirScopes.ReadPatient);
var request = new HttpRequestMessage(
HttpMethod.Get,
$"{server.BaseUrl}/Patient/{patientId}");
request.Headers.Authorization =
new AuthenticationHeaderValue(
"Bearer", token);
var response = await _httpClient
.SendAsync(request);
response.EnsureSuccessStatusCode();
var json = await response.Content
.ReadAsStringAsync();
return _serializer.Deserialize<Patient>(json);
}
public async Task<Encounter>
CreateTelehealthEncounterAsync(
EncounterRequest request,
FhirServerConfig server)
{
var token = await _auth.GetAccessTokenAsync(
server, FhirScopes.WriteEncounter);
var encounter = new Encounter
{
Status = EncounterStatus.InProgress,
Class = new Coding
{
System = "http://terminology.hl7.org/" +
"CodeSystem/v3-ActCode",
Code = "VR",
Display = "Virtual Encounter"
},
Subject = new Reference
{
Reference =
$"Patient/{request.PatientId}"
},
Participant = new List<
Encounter.ParticipantComponent>
{
new Encounter.ParticipantComponent
{
Individual = new Reference
{
Reference =
$"Practitioner/{request.ProviderId}"
}
}
},
Period = new Period
{
Start = DateTimeOffset.UtcNow
.ToString("o")
},
ServiceProvider = new Reference
{
Reference =
$"Organization/{request.OrganizationId}"
}
};
var content = new StringContent(
_serializer.Serialize(encounter),
Encoding.UTF8,
"application/fhir+json");
var httpRequest = new HttpRequestMessage(
HttpMethod.Post,
$"{server.BaseUrl}/Encounter")
{ Content = content };
httpRequest.Headers.Authorization =
new AuthenticationHeaderValue(
"Bearer", token);
var response = await _httpClient
.SendAsync(httpRequest);
response.EnsureSuccessStatusCode();
var json = await response.Content
.ReadAsStringAsync();
return _serializer
.Deserialize<Encounter>(json);
}
}
Key FHIR Resources
| FHIR Resource | Purpose | Service |
|---|---|---|
| Patient | Demographics, contact, insurance | User, EHR Integration |
| Practitioner | Provider credentials | Provider Service |
| Encounter | Visit record | EHR Integration |
| Observation | Vitals, lab results | Vitals, Lab Service |
| MedicationRequest | Prescriptions | Prescription Service |
| ServiceRequest | Lab/imaging orders | Lab Service |
| DocumentReference | Clinical notes | Notes Service |
| Condition | Diagnoses | Notes Service |
| AllergyIntolerance | Allergies | EHR Integration |
| Coverage | Insurance | Insurance Service |
| Claim | Billing | Billing Service |
10. Prescription Management & E-Prescribing
E-prescribing sends prescriptions electronically to pharmacies via Surescripts. EPCS (Electronic Prescribing for Controlled Substances) requires DEA compliance, 2FA, and tamper-evident audit trails.
C#
public class PrescriptionService
{
private readonly IPrescriptionRepository _repo;
private readonly IDrugInteractionService _drugChecker;
private readonly ISurescriptsClient _surescripts;
private readonly IAuditLogger _auditLogger;
public async Task<PrescriptionResult>
CreatePrescriptionAsync(
CreatePrescriptionRequest request)
{
var authority =
await ValidatePrescribingAuthorityAsync(
request.ProviderId, request.Medication);
if (!authority.IsAuthorized)
return PrescriptionResult.Failure(
authority.Reason);
var currentMeds = await _repo
.GetActiveMedicationsAsync(
request.PatientId);
var interactions = await _drugChecker
.CheckInteractionsAsync(
request.Medication, currentMeds);
var major = interactions.Where(i =>
i.Severity == InteractionSeverity.Major
|| i.Severity
== InteractionSeverity.Contraindicated)
.ToList();
if (major.Any()
&& !request.OverrideJustification
.HasValue)
{
return PrescriptionResult
.RequiresOverride(major);
}
var prescription = new Prescription
{
Id = Guid.NewGuid(),
PatientId = request.PatientId,
ProviderId = request.ProviderId,
Medication = request.Medication,
Sig = BuildSigInstruction(request),
Quantity = request.Quantity,
Refills = request.Refills,
PharmacyNCPDPId =
request.PharmacyNCPDPId,
Status = PrescriptionStatus.Pending,
IsControlled =
IsControlledSubstance(
request.Medication.DEASchedule),
Interactions = interactions,
CreatedAt = DateTime.UtcNow
};
await _repo.SaveAsync(prescription);
var txResult = await _surescripts
.TransmitPrescriptionAsync(prescription);
prescription.Status = txResult.Success
? PrescriptionStatus.Sent
: PrescriptionStatus.TransmissionFailed;
await _repo.UpdateAsync(prescription);
await _auditLogger.LogAsync(new AuditEvent
{
Action = "prescription.create",
ResourceId = prescription.Id,
PatientId = request.PatientId,
Details = new
{
Medication = request.Medication.Name,
IsControlled =
prescription.IsControlled
}
});
return PrescriptionResult
.Success(prescription);
}
}
11. Lab Orders & Results
Virtual visits often result in lab orders at local draw centers. The workflow: create order, transmit to lab network, track fulfillment, receive results, flag abnormal values, and present to provider for review.
Lab Order Lifecycle
| Status | Description | Actor |
|---|---|---|
| Drafted | Provider creates order during visit | Provider |
| Signed | Provider signs and transmits | Provider |
| Transmitted | Sent to lab network (HL7 ORM) | System |
| Received | Lab acknowledges | Lab |
| In Progress | Sample collected at draw center | Patient + Lab |
| Results Ready | Analysis complete | Lab |
| Provider Reviewed | Provider documents review | Provider |
| Patient Notified | Results shared via portal | System |
C#
public class LabOrderService
{
public async Task ProcessLabResultsAsync(
Hl7Message labResult)
{
var result = ParseLabResult(labResult);
var observations = result.Observations
.Select(obs => new LabObservation
{
LOINCCode = obs.LoincCode,
ComponentName = obs.ComponentName,
Value = obs.Value,
Unit = obs.Unit,
ReferenceRange = obs.ReferenceRange,
AbnormalFlag = ClassifyAbnormal(obs)
}).ToList();
var criticals = observations
.Where(o => o.AbnormalFlag
== AbnormalFlag.Critical)
.ToList();
if (criticals.Any())
{
await _notifications
.SendCriticalLabAlertAsync(
result.OrderingProviderId,
result.PatientId, criticals);
}
var doc = new LabResultDocument
{
OrderId = result.OrderId,
PatientId = result.PatientId,
ProviderId = result.OrderingProviderId,
Observations = observations,
Status = LabResultStatus.Available
};
await _repo.SaveLabResultAsync(doc);
await _fhirClient.CreateObservationsAsync(
observations, result.PatientId);
}
}
12. Insurance Verification & Eligibility
Real-time verification prevents claim denials and collects accurate copays. Before check-in, verify eligibility, determine copay/coinsurance, check prior auth requirements, and identify network status.
C#
public class InsuranceVerificationService
{
private readonly IClearinghouseClient _clearinghouse;
private readonly ICacheService _cache;
public async Task<VerificationResult>
VerifyEligibilityAsync(
InsuranceVerificationRequest request)
{
var cacheKey =
$"insurance:verify:{request.InsuranceId}";
var cached = await _cache
.GetAsync<VerificationResult>(cacheKey);
if (cached != null) return cached;
var edi270 =
BuildEligibilityRequest(request);
var edi271 = await _clearinghouse
.SubmitEligibilityRequestAsync(edi270);
var result = new VerificationResult
{
IsEligible =
edi271.EligibilityStatus == "Active",
PlanName = edi271.PlanName,
CopayAmount = edi271.CopayAmount,
CoinsurancePercent =
edi271.CoinsurancePercent,
DeductibleMet =
edi271.DeductibleRemaining == 0,
DeductibleRemaining =
edi271.DeductibleRemaining,
PriorAuthRequired =
edi271.PriorAuthRequired,
InNetworkProvider =
await CheckNetworkStatusAsync(
request.ProviderNPI,
request.Insurance),
VerifiedAt = DateTime.UtcNow
};
await _cache.SetAsync(cacheKey, result,
TimeSpan.FromHours(24));
return result;
}
}
13. Payment Processing & Billing
Healthcare billing involves copay collection, insurance claim submission, post-adjudication patient responsibility, and coordination of benefits.
C#
public class BillingService
{
public async Task<Claim> GenerateClaimAsync(
Guid appointmentId)
{
var appt = await _appointments
.GetByIdAsync(appointmentId);
var note = await _notes
.GetByAppointmentAsync(appointmentId);
var superbill = new Superbill
{
AppointmentId = appointmentId,
PatientId = appt.PatientId,
ProviderId = appt.ProviderId,
ProviderNPI =
appt.Provider.NpiNumber,
DateOfService =
appt.ScheduledAt.Date,
CPTCodes = note.CptCodes
.Select(code => new CptEntry
{
Code = code,
Description =
CptLookup.GetDescription(code),
Fee = GetFee(code, appt.Provider)
}).ToList(),
ICD10Codes = note.Assessment
.Where(a => a.IsPrimary)
.Select(a => new Icd10Entry
{
Code = a.Icd10Code,
Description = a.Diagnosis,
Order = 1
}).ToList(),
PlaceOfService = "02",
PayerId =
appt.Patient.Insurance.PayerId,
MemberId =
appt.Patient.Insurance.MemberId
};
var edi837 = GenerateEdi837P(superbill);
var result = await _clearinghouse
.SubmitClaimAsync(edi837);
return new Claim
{
Id = Guid.NewGuid(),
Superbill = superbill,
Status = ClaimStatus.Submitted,
SubmissionId = result.ClaimId
};
}
}
Stripe Payment Integration
C#
public class PaymentService
{
private readonly StripeClient _stripe;
public async Task<PaymentResult> CollectCopayAsync(
CollectCopayRequest request)
{
var customerId =
await GetOrCreateStripeCustomerAsync(
request.PatientId);
var intent = await _stripe.PaymentIntents
.CreateAsync(new PaymentIntentCreateOptions
{
Amount = ConvertToCents(request.Amount),
Currency = "usd",
Customer = customerId,
Description =
$"Copay for visit {request.VisitDate:d}",
Metadata = new Dictionary<string, string>
{
{ "patient_id",
request.PatientId.ToString() },
{ "appointment_id",
request.AppointmentId.ToString() }
},
AutomaticPaymentMethods =
new PaymentIntentAutomaticPaymentMethodsOptions
{ Enabled = true }
});
return new PaymentResult
{
PaymentIntentId = intent.Id,
ClientSecret = intent.ClientSecret,
Amount = request.Amount
};
}
}
14. Secure Messaging & File Sharing
Secure messaging enables async communication between patients and providers. Use cases: follow-up questions, sharing symptom photos, discussing lab results. All must be encrypted, HIPAA-compliant, with complete audit trail.
C#
public class MessagingService : IMessagingService
{
private readonly IMessageRepository _messages;
private readonly IFileStorageService _fileStorage;
private readonly IPushNotificationService _push;
private readonly IAuditLogger _auditLogger;
public async Task<Message> SendMessageAsync(
SendMessageRequest request)
{
var conversation = await ValidateAccessAsync(
request.ConversationId, request.SenderId);
var attachments =
new List<MessageAttachment>();
foreach (var file in request.Attachments)
{
if (!AllowedFileTypes.Contains(
Path.GetExtension(file.FileName)
.ToLower()))
throw new ValidationException(
"File type not allowed");
if (file.Length > 25 * 1024 * 1024)
throw new ValidationException(
"File exceeds 25MB limit");
var encrypted = await _encryption
.EncryptAsync(file.Bytes);
var path = await _fileStorage
.StoreAsync(encrypted,
$"messages/{request.ConversationId}");
attachments.Add(new MessageAttachment
{
Id = Guid.NewGuid(),
FileName = file.FileName,
ContentType = file.ContentType,
FileSize = file.Length,
StoragePath = path
});
}
var message = new Message
{
Id = Guid.NewGuid(),
ConversationId =
request.ConversationId,
SenderId = request.SenderId,
Content = request.Content,
Attachments = attachments,
SentAt = DateTime.UtcNow,
ReadBy = new List<MessageReadReceipt>
{
new MessageReadReceipt
{
UserId = request.SenderId,
ReadAt = DateTime.UtcNow
}
}
};
await _messages.SaveAsync(message);
var recipients = conversation.Participants
.Where(p => p.UserId
!= request.SenderId)
.ToList();
foreach (var r in recipients)
{
await _push.SendAsync(new PushNotification
{
UserId = r.UserId,
Title =
$"New message from {request.SenderName}",
Body = request.Content.Length > 100
? request.Content[..100] + "..."
: request.Content
});
}
await _auditLogger.LogAsync(new AuditEvent
{
Action = "message.send",
ResourceId = message.Id,
PatientId = conversation.PatientId
});
return message;
}
}
File Sharing Specifications
| File Type | Max Size | Formats | Retention |
|---|---|---|---|
| Medical Images | 50 MB | DICOM, JPEG, PNG | 10 years |
| Lab Results | 10 MB | 10 years | |
| Insurance Cards | 5 MB | JPEG, PNG, PDF | Coverage + 2 years |
| Consent Forms | 5 MB | 10 years | |
| General | 25 MB | PDF, JPEG, PNG | 7 years |
15. Multi-Party Calls & Specialist Referrals
Virtual visits sometimes require more than two participants: family members, specialists for referral consultations, medical interpreters, or care coordinators. The multi-party system extends the WebRTC infrastructure to support these scenarios.
C#
public class MultiPartyCallService
{
private readonly ISfuClient _sfuClient;
private readonly IReferralService _referrals;
private readonly INotificationService _notifications;
public async Task<ReferralResult>
InitiateReferralAsync(ReferralRequest request)
{
var specialist =
await FindAvailableSpecialistAsync(
request.SpecialtyNeeded,
request.PatientInsurance,
request.UrgencyLevel);
if (specialist == null)
return ReferralResult.NoSpecialistAvailable();
var referral = await _referrals.CreateAsync(
new Referral
{
PatientId = request.PatientId,
ReferringProviderId =
request.CurrentProviderId,
SpecialistId = specialist.Id,
Reason = request.Reason,
ClinicalContext =
request.ClinicalSummary,
Urgency = request.UrgencyLevel
});
await _notifications.SendReferralInviteAsync(
specialist.Id, new ReferralInvite
{
ReferralId = referral.Id,
PatientName = request.PatientName,
JoinUrl = await _sfuClient
.GenerateJoinTokenAsync(
request.CurrentSessionId,
specialist.Id,
ParticipantRole.Specialist)
});
var accepted =
await WaitForAcceptanceAsync(
referral.Id,
TimeSpan.FromMinutes(5));
if (!accepted)
return ReferralResult.SpecialistDeclined();
var participant = await _sfuClient
.AddParticipantAsync(
request.CurrentSessionId,
new ParticipantConfig
{
Id = specialist.Id,
Role = ParticipantRole.Specialist,
CanPublish = true,
CanSubscribe = true
});
return ReferralResult.Success(
referral, participant);
}
}
16. Session Recording & Consent
Recording is used for clinical documentation review, QA, training, and patient reference. Explicit informed consent must be obtained from all parties before recording begins, and recordings must be stored with the same HIPAA protections as any other PHI.
C#
public class RecordingService
{
private readonly IConsentService _consent;
private readonly ISfuClient _sfu;
private readonly IEncryptedStorage _storage;
public async Task<RecordingResult>
StartRecordingAsync(
StartRecordingRequest request)
{
var participants = await _sfu
.GetParticipantsAsync(request.SessionId);
foreach (var p in participants)
{
var status = await _consent
.GetConsentStatusAsync(
p.Id, ConsentType.Recording);
if (status != ConsentStatus.Active)
return RecordingResult
.ConsentRequired(p.Id);
}
var config = new RecordingConfig
{
SessionId = request.SessionId,
Layout = RecordingLayout.Composite,
OutputFormat = RecordingFormat.WebM,
EncryptionKey = await _storage
.GenerateEncryptionKeyAsync(
request.SessionId),
StoragePath =
$"recordings/{request.SessionId}"
};
var recording = await _sfu
.StartRecordingAsync(config);
return RecordingResult.Started(recording.Id);
}
}
17. Clinical Notes (SOAP Notes)
Clinical documentation is the primary legal record of a medical encounter. SOAP notes (Subjective, Objective, Assessment, Plan) are the standard format. The system supports real-time editing, auto-save, coding suggestions, and sign-off workflow.
SOAP Note Structure
| Section | Content | Data Type |
|---|---|---|
| Subjective | Chief complaint, HPI, ROS | Free text + structured |
| Objective | Vitals, exam findings | Numeric + free text |
| Assessment | Diagnoses, ICD-10 codes | Structured + free text |
| Plan | Meds, orders, referrals | Structured + free text |
C#
public class ClinicalNotesService
{
private readonly IMongoCollection<ClinicalNote> _notes;
private readonly ICodeSuggestionEngine _codingEngine;
public async Task<ClinicalNote> CreateNoteAsync(
CreateNoteRequest request)
{
var note = new ClinicalNote
{
Id = ObjectId.GenerateNewId(),
AppointmentId = request.AppointmentId,
PatientId = request.PatientId,
ProviderId = request.ProviderId,
NoteType = NoteType.SOAP,
Status = NoteStatus.Draft,
Subjective = new SubjectiveSection
{
ChiefComplaint = request.ChiefComplaint,
HistoryOfPresentIllness = request.HPI,
ReviewOfSystems = request.ROS
},
Objective = new ObjectiveSection(),
Assessment = new AssessmentSection(),
Plan = new PlanSection(),
Version = 1,
CreatedAt = DateTime.UtcNow
};
await _notes.InsertOneAsync(note);
return note;
}
public async Task<CodingSuggestion>
SuggestCodesAsync(Guid noteId)
{
var note = await _notes
.Find(n => n.Id == noteId)
.FirstOrDefaultAsync();
var suggestion = await _codingEngine
.AnalyzeAsync(new CodingRequest
{
ChiefComplaint =
note.Subjective.ChiefComplaint,
HPI = note.Subjective
.HistoryOfPresentIllness,
ExamFindings =
note.Objective.Examination,
ExistingDiagnoses =
note.Assessment.Diagnoses,
MedicationsOrdered =
note.Plan.Medications
});
return new CodingSuggestion
{
NoteId = noteId,
ICD10Suggestions = suggestion.Diagnoses
.Select(d => new Icd10Suggestion
{
Code = d.Code,
Description = d.Description,
Confidence = d.Confidence,
IsPrimary = d.IsPrimary
}).ToList(),
CPTSuggestions = suggestion.Services
.Select(s => new CptSuggestion
{
Code = s.Code,
Description = s.Description,
Confidence = s.Confidence
}).ToList()
};
}
public async Task SignNoteAsync(
Guid noteId, Guid providerId)
{
var note = await _notes
.Find(n => n.Id == noteId)
.FirstOrDefaultAsync();
if (note == null)
throw new NotFoundException("Note not found");
if (note.ProviderId != providerId)
throw new ForbiddenException(
"Only author can sign");
var validation = ValidateForSigning(note);
if (!validation.IsValid)
throw new ValidationException(
"Cannot sign", validation.Errors);
var update = Builders<ClinicalNote>.Update
.Set(n => n.Status, NoteStatus.Signed)
.Set(n => n.Signatures,
new List<NoteSignature>
{
new NoteSignature
{
ProviderId = providerId,
SignedAt = DateTime.UtcNow
}
})
.Set(n => n.UpdatedAt, DateTime.UtcNow)
.Inc(n => n.Version, 1);
await _notes.UpdateOneAsync(
n => n.Id == noteId, update);
}
}
18. Triage, Symptom Checker & AI-Assisted Routing
When a patient requests an on-demand visit, the platform must assess acuity, determine care level, and route to the right provider. AI triage uses symptom checkers, clinical rules, and historical data.
C#
public class TriageService
{
private readonly ISymptomNlpEngine _nlp;
private readonly IClinicalRuleEngine _rules;
private readonly IRoutingEngine _routing;
public async Task<TriageResult> AssessAsync(
TriageRequest request)
{
var extracted = await _nlp
.ExtractSymptomsAsync(
request.SymptomDescription);
var allSymptoms = extracted
.Concat(request.SelectedSymptoms)
.GroupBy(s => s.SnomedCode)
.Select(g => g.First()).ToList();
var ruleResults = await _rules
.EvaluateAsync(new ClinicalRuleInput
{
Symptoms = allSymptoms,
Age = request.PatientAge,
Sex = request.PatientSex,
Vitals = request.CurrentVitals,
MedicalHistory =
request.RelevantHistory,
Medications =
request.CurrentMedications
});
var acuity = CalculateAcuity(
allSymptoms, ruleResults, request);
var routing = await _routing.DecideAsync(
new RoutingInput
{
AcuityScore = acuity,
Symptoms = allSymptoms,
RuleResults = ruleResults,
TimeOfDay = DateTime.UtcNow
});
return new TriageResult
{
AcuityLevel = acuity.Level,
AcuityScore = acuity.Score,
RecommendedSpecialty =
routing.Specialty,
SafetyAlerts =
ruleResults.SafetyAlerts,
RedFlags = ruleResults.RedFlags,
RequiresInPerson =
ruleResults.RedFlags.Any()
|| acuity.Level
== AcuityLevel.Emergency
};
}
private AcuityScore CalculateAcuity(
List<ExtractedSymptom> symptoms,
RuleEvaluationResult rules,
TriageRequest request)
{
double score = 0;
if (request.CurrentVitals != null)
{
if (request.CurrentVitals.HeartRate > 120
|| request.CurrentVitals
.HeartRate < 50)
score += 3;
if (request.CurrentVitals
.OxygenSaturation < 94)
score += 4;
if (request.CurrentVitals
.Temperature > 103)
score += 3;
}
foreach (var s in symptoms)
score += s.Severity * s.UrgencyWeight;
if (rules.CriticalFindings.Any())
score += 5;
var level = score switch
{
>= 10 => AcuityLevel.Emergency,
>= 7 => AcuityLevel.Urgent,
>= 4 => AcuityLevel.SemiUrgent,
>= 2 => AcuityLevel.Routine,
_ => AcuityLevel.NonUrgent
};
return new AcuityScore
{
Score = score, Level = level
};
}
}
19. Vitals Monitoring & Connected Devices
Remote patient monitoring through Bluetooth-connected devices extends the platform beyond synchronous visits: blood pressure cuffs, pulse oximeters, glucometers, thermometers, and smartwatches.
C#
public class VitalsIngestionService
{
private readonly IKafkaProducer _kafka;
private readonly IAlertEngine _alertEngine;
private readonly IDeviceRegistry _devices;
public async Task<IngestResult>
IngestVitalsAsync(VitalsReading reading)
{
var device = await _devices.ValidateDeviceAsync(
reading.DeviceId, reading.PatientId);
if (device == null)
return IngestResult.UnauthorizedDevice();
var normalized = new NormalizedVitalsReading
{
Id = Guid.NewGuid(),
PatientId = reading.PatientId,
DeviceId = reading.DeviceId,
DeviceType = device.Type,
MetricType = reading.MetricType,
Value = reading.Value,
Unit = reading.Unit,
Timestamp = reading.Timestamp,
ReceivedAt = DateTime.UtcNow
};
await _kafka.ProduceAsync("vitals.raw",
reading.PatientId.ToString(), normalized);
var alerts = await _alertEngine.EvaluateAsync(normalized);
if (alerts.Any())
await _alertEngine.DispatchAlertsAsync(
alerts, reading.PatientId);
return IngestResult.Success(normalized.Id);
}
}
Supported Devices
| Device | Metrics | Normal | Critical |
|---|---|---|---|
| BP Cuff | Systolic/Diastolic | 90-140/60-90 | >180/120 |
| Pulse Ox | SpO2, HR | 95-100%, 60-100 | SpO2 <90% |
| Glucometer | Blood Glucose | 70-140 mg/dL | >400 or <54 |
| Thermometer | Temperature | 97-99F | >104F |
| Smart Scale | Weight, BMI | Baseline +/-5% | >5 lbs/day |
| Smartwatch | HR, HRV, Steps | Baseline | Afib, anomaly |
20. Security, HIPAA Compliance & Encryption
Security is a regulatory mandate. HIPAA and HITECH impose strict requirements on PHI. A single breach can mean fines of $100-$50,000 per record.
C#
[AttributeUsage(AttributeTargets.Property)]
public class EncryptedPhiAttribute : Attribute
{
public string KeyAlias { get; }
public EncryptedPhiAttribute(string keyAlias)
{
KeyAlias = keyAlias;
}
}
public class Patient
{
public Guid Id { get; set; }
[EncryptedPhi("patient-name")]
public string FirstName { get; set; }
[EncryptedPhi("patient-name")]
public string LastName { get; set; }
[EncryptedPhi("patient-dob")]
public DateTime DateOfBirth { get; set; }
[EncryptedPhi("patient-ssn")]
public string SSN { get; set; }
[EncryptedPhi("patient-contact")]
public string Email { get; set; }
}
HIPAA Compliance Checklist
| Requirement | Implementation | Status |
|---|---|---|
| Encryption at rest | AES-256 via KMS | Done |
| Encryption in transit | TLS 1.3 everywhere | Done |
| Access controls | RBAC + ABAC | Done |
| Audit logging | 100% PHI access, 7yr | Done |
| BAAs | All vendors | Done |
| Breach notification | 60-day workflow | Done |
| Minimum necessary | Field-level per role | Done |
| De-identification | Safe Harbor | Done |
| DR | RPO <1min, RTO <15min | Done |
| Training | Annual + phishing | Done |
Audit Logger
C#
public class AuditLogger : IAuditLogger
{
private readonly IAuditRepository _repo;
private readonly IKafkaProducer _kafka;
public async Task LogAsync(AuditEvent evt)
{
var enriched = new AuditLogEntry
{
EventId = Guid.NewGuid(),
EventTime = DateTime.UtcNow,
ActorId = evt.ActorId,
Action = evt.Action,
ResourceType = evt.ResourceType,
ResourceId = evt.ResourceId,
PatientId = evt.PatientId,
IPAddress = _http.ClientIp(),
Details = Sanitize(evt.Details)
};
await _kafka.ProduceAsync(
"audit.log", enriched.ActorId, enriched);
await _repo.AppendAsync(enriched);
}
private JsonElement Sanitize(JsonElement details)
{
var json = details.GetRawText();
json = Regex.Replace(json,
@"\b\d{3}-\d{2}-\d{4}\b",
"[SSN_REDACTED]");
return JsonSerializer.Deserialize<JsonElement>(json);
}
}
21. API Design
RESTful APIs with OpenAPI 3.1 specs. Internal gRPC. External REST/JSON.
Core Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v1/providers/search | Search providers | JWT |
| GET | /api/v1/providers/{id}/slots | Available slots | JWT |
| POST | /api/v1/appointments | Create appointment | JWT |
| POST | /api/v1/waiting-room/check-in | Check in | JWT |
| POST | /api/v1/video/sessions | Create session | JWT+Role |
| GET | /api/v1/patients/{id}/ehr/summary | FHIR summary | JWT+Scope |
| POST | /api/v1/prescriptions | Create Rx | JWT+Role |
| POST | /api/v1/lab-orders | Create lab order | JWT+Role |
| POST | /api/v1/insurance/verify | Verify eligibility | JWT |
| POST | /api/v1/payments | Process payment | JWT |
| POST | /api/v1/messages | Send message | JWT |
| POST | /api/v1/clinical-notes | Create note | JWT+Role |
| POST | /api/v1/clinical-notes/{id}/sign | Sign note | JWT+Role |
| POST | /api/v1/triage/assess | AI triage | JWT |
| POST | /api/v1/vitals | Ingest vitals | Device |
| POST | /api/v1/consent | Capture consent | JWT |
| GET | /api/v1/audit/patient/{id} | PHI audit log | Admin |
Request/Response
HTTP
POST /api/v1/appointments
Content-Type: application/json
{
"providerId": "550e8400-e29b-41d4-a716-446655440000",
"slotStart": "2026-07-15T14:00:00Z",
"durationMinutes": 30,
"visitType": "follow_up",
"insuranceId": "ins-abc-123",
"consentGiven": true
}
// 201 Created
{
"id": "apt-789xyz",
"status": "scheduled",
"provider": { "name": "Dr. Sarah Johnson" },
"scheduledAt": "2026-07-15T14:00:00Z",
"insuranceVerification": { "copayAmount": 30.00 },
"payment": { "paymentIntentId": "pi_xyz_123" }
}
Rate Limiting
| Category | Limit | Window |
|---|---|---|
| Search | 60 | 1 min |
| Booking | 10 | 1 min |
| Payment | 5 | 1 min |
| Video | 100 | 1 min |
| Messaging | 30 | 1 min |
| Vitals | 120 | 1 min |
| Triage | 5 | 5 min |
22. Cost Estimation
HIPAA-compliant infrastructure is expensive due to encryption, media servers, compliance, and HA.
Monthly (10K Patients, 50 Providers)
| Category | Service | Cost |
|---|---|---|
| Compute | EKS 8 nodes | $2,800 |
| DB | RDS PostgreSQL Multi-AZ | $1,200 |
| DB | DocumentDB | $600 |
| Cache | ElastiCache Redis | $900 |
| MQ | MSK Kafka | $1,200 |
| Storage | S3 | $120 |
| Media | LiveKit Cloud | $5,000 |
| CDN | CloudFront + WAF | $300 |
| Search | OpenSearch | $800 |
| Rx | Surescripts | $500 |
| Insurance | Clearinghouse | $400 |
| Comms | Twilio | $600 |
| SendGrid | $100 | |
| AI | Bedrock | $400 |
| Payments | Stripe | 2.9%+$0.30 |
| Monitoring | CloudWatch+Datadog | $500 |
| Security | KMS+GuardDuty | $400 |
| Compliance | Vanta | $1,500 |
| CI/CD | Tooling | $300 |
| Total | $18K-$22K/mo | |
Cost Optimization
The largest cost drivers are EKS compute, RDS, and TURN relay bandwidth. The following strategies can reduce total monthly spend by 30–40% without sacrificing reliability or HIPAA compliance. Most savings come from compute commitments, storage lifecycle management, and intelligent caching at the edge.
- Reserved Instances: Commit to 1-year RDS and EKS node reservations for 30–40% savings on compute. For a platform spending roughly $2,800/month on EKS alone, a 1-year commitment reduces this to approximately $1,700/month — a direct $13,200 annual saving.
- Spot Instances: Use spot instances for non-critical batch workflows such as insurance claim processing, clinical data de-identification jobs, and nightly analytics pipelines. These workloads tolerate interruptions and can checkpoint their progress via SQS or Step Functions.
- CDN Caching: Cache static assets aggressively at the edge: provider profile photos, consent form templates, educational health content, and application JavaScript bundles. This reduces origin server load by 60–70% and cuts CloudFront bandwidth costs significantly.
- Tiered Storage: Move session recordings older than 90 days to S3 Glacier Deep Archive, reducing storage costs by 90% for infrequently accessed recordings while maintaining HIPAA-compliant encryption at rest.
- Lazy Loading: Load EHR summaries on-demand rather than pre-fetching for every scheduled visit, reducing FHIR API calls to external EHR systems and associated latency. Cache summaries in Redis only for active visits within a 30-minute window.
- Shared TURN Infrastructure: Partner with a managed TURN relay service (Twilio Network Traversal, Metered.ca) for shared relay capacity instead of maintaining dedicated TURN servers in every region, reducing relay hosting costs by 40–50%.
23. Testing Strategy
Testing a telehealth platform requires functional correctness, real-time performance, security, and healthcare integration testing. Bugs can harm patients.
Testing Pyramid
| Layer | Type | Coverage | Tools |
|---|---|---|---|
| Unit | Logic, calculations | 90%+ | xUnit, Moq |
| Integration | Service-to-service | 80%+ | Testcontainers |
| Contract | API contracts, FHIR | 100% | Pact, FHIR validator |
| E2E | User journeys | Critical | Playwright |
| Load | Concurrent sessions | Peak+2x | k6 |
| Security | SAST, DAST, pen test | 0 critical | SonarQube, ZAP |
| Compliance | HIPAA controls | All | Vanta + custom |
Key Scenarios
- Double-Booking: Two patients booking same slot
- Video Failover: Kill TURN server mid-call, reconnect in 5s
- Drug Interactions: Conflicting Rx must be flagged
- Insurance Timeout: Clearinghouse >30s, async retry
- PHI Leak Detection: Automated log scanning
- Consent Before Recording: Block without consent
- State Licensing: Block cross-state violations
C#
[Fact]
public async Task Should_Prevent_Double_Booking()
{
var slot = await CreateAvailableSlot(
providerId, testDate, testTime);
var task1 = bookingService.BookAsync(
new BookingRequest
{
SlotId = slot.Id, PatientId = patient1Id
});
var task2 = bookingService.BookAsync(
new BookingRequest
{
SlotId = slot.Id, PatientId = patient2Id
});
var results = await Task.WhenAll(task1, task2);
Assert.Equal(1, results.Count(r => r.IsSuccess));
var slotAfter = await GetSlotAsync(slot.Id);
Assert.Equal(SlotStatus.Booked, slotAfter.Status);
}
[Fact]
public async Task Should_Log_PHI_Access()
{
var patient = await CreateTestPatient();
var provider = await CreateTestProvider();
await patientService.GetPatientAsync(
patient.Id, provider.Id);
var logs = await auditRepo
.GetByPatientAsync(patient.Id);
Assert.Contains(logs, e =>
e.Action == "phi.read.patient"
&& e.ActorId == provider.Id);
foreach (var entry in logs)
{
var json = JsonSerializer.Serialize(entry.Details);
Assert.DoesNotContain(patient.FirstName, json);
}
}
25. Provider Credentialing & Privileging
Provider credentialing is the process of verifying a healthcare provider's qualifications, training, licensure, and competency before they can deliver care through the platform. This is a legal requirement under state and federal law, and a critical patient safety measure. The credentialing process must verify primary sources, track expiration dates, and manage re-credentialing cycles.
Credentialing Verification Steps
| Step | Verification | Source | SLA |
|---|---|---|---|
| 1 | Medical school graduation | AMA Physician Masterfile or medical school | 5 business days |
| 2 | Residency/fellowship completion | Training institution or ABMS | 5 business days |
| 3 | State medical license | State medical board | Real-time API where available |
| 4 | DEA registration | DEA Active Registrants database | Real-time API |
| 5 | Board certification | ABMS or AOA | Real-time API |
| 6 | NPI number | NPPES NPI Registry | Real-time API |
| 7 | Malpractice history | National Practitioner Data Bank (NPDB) | 10 business days |
| 8 | Criminal background | FBI/state background check | 15 business days |
| 9 | OIG/SAM exclusion check | OIG LEIE and SAM.gov | Real-time |
| 10 | Work history (5 years) | Previous employers | 15 business days |
C#
public class CredentialingService
{
private readonly ICredentialingRepository _repo;
private readonly INpiRegistryClient _npiRegistry;
private readonly IDeaRegistryClient _deaRegistry;
private readonly IAbmsClient _abmsClient;
private readonly IOigExclusionClient _oigClient;
private readonly IAuditLogger _auditLogger;
public async Task<CredentialingResult>
RunCredentialingAsync(CredentialingRequest req)
{
var provider = await _repo
.GetProviderAsync(req.ProviderId);
var results =
new List<VerificationResult>();
// NPI verification (real-time)
var npi = await _npiRegistry
.VerifyNpiAsync(provider.NpiNumber);
results.Add(new VerificationResult
{
Step = "NPI Verification",
Status = npi.IsValid
? VerificationStatus.Passed
: VerificationStatus.Failed,
Details = npi.IsValid
? $"Active - {npi.EnumerationType}"
: "NPI not found or inactive",
VerifiedAt = DateTime.UtcNow
});
// DEA verification (real-time)
if (!string.IsNullOrEmpty(provider.DeaNumber))
{
var dea = await _deaRegistry
.VerifyDeaAsync(
provider.DeaNumber,
provider.LicenseState);
results.Add(new VerificationResult
{
Step = "DEA Verification",
Status = dea.IsActive
? VerificationStatus.Passed
: VerificationStatus.Failed,
Details = dea.IsActive
? $"Schedules: {dea.ActiveSchedules}"
: "DEA registration inactive",
VerifiedAt = DateTime.UtcNow
});
}
// Board certification (real-time)
var board = await _abmsClient
.VerifyBoardCertificationAsync(
provider.NpiNumber,
provider.Specialty);
results.Add(new VerificationResult
{
Step = "Board Certification",
Status = board.IsCertified
? VerificationStatus.Passed
: VerificationStatus.NotApplicable,
Details = board.IsCertified
? $"Certified by {board.BoardName} "
+ $"since {board.CertificationDate:d}"
: "Not board certified",
VerifiedAt = DateTime.UtcNow
});
// OIG/SAM exclusion check (real-time)
var oig = await _oigClient
.CheckExclusionAsync(
provider.NpiNumber,
provider.FirstName,
provider.LastName);
results.Add(new VerificationResult
{
Step = "OIG Exclusion Check",
Status = oig.IsExcluded
? VerificationStatus.Failed
: VerificationStatus.Passed,
Details = oig.IsExcluded
? $"EXCLUDED: {oig.ExclusionReason}"
: "Not excluded",
VerifiedAt = DateTime.UtcNow
});
// State license verification
var license = await VerifyStateLicenseAsync(
provider.LicenseState,
provider.LicenseNumber,
provider.Specialty);
results.Add(new VerificationResult
{
Step = "State License",
Status = license.IsActive
? VerificationStatus.Passed
: VerificationStatus.Failed,
Details = license.IsActive
? $"Active until {license.Expiry:d}"
: $"License {license.Status}",
VerifiedAt = DateTime.UtcNow
});
// Determine overall status
var hasFailures = results.Any(r =>
r.Status == VerificationStatus.Failed);
var overallStatus = hasFailures
? CredentialingStatus.Rejected
: CredentialingStatus.Approved;
// Update provider credentialing status
provider.CredentialingStatus = overallStatus;
provider.CredentialingCompletedAt =
DateTime.UtcNow;
provider.CredentialingResults = results;
await _repo.UpdateProviderAsync(provider);
await _auditLogger.LogAsync(new AuditEvent
{
Action = "credentialing.complete",
ResourceId = req.ProviderId,
Details = new
{
Status = overallStatus,
FailedSteps = results
.Where(r => r.Status
== VerificationStatus.Failed)
.Select(r => r.Step)
.ToList()
}
});
return new CredentialingResult
{
ProviderId = req.ProviderId,
Status = overallStatus,
VerificationSteps = results,
CompletedAt = DateTime.UtcNow,
NextRenewalDate = DateTime.UtcNow
.AddYears(2)
};
}
public async Task ScheduleReCredentialingAsync(
Guid providerId)
{
var provider = await _repo
.GetProviderAsync(providerId);
// Re-credential every 2 years
var nextDue = provider.CredentialingCompletedAt
.AddYears(2);
// Send reminders at 90, 60, 30 days
await ScheduleReminderAsync(providerId,
nextDue.AddDays(-90),
"Credentialing renewal due in 90 days");
await ScheduleReminderAsync(providerId,
nextDue.AddDays(-60),
"Credentialing renewal due in 60 days");
await ScheduleReminderAsync(providerId,
nextDue.AddDays(-30),
"Credentialing renewal due in 30 days - "
+ "urgent");
// Block scheduling if not completed by due date
await ScheduleBlockAsync(providerId,
nextDue,
"Credentialing expired - scheduling blocked");
}
}
26. Patient Consent Management
Informed consent is a legal and ethical requirement before any medical treatment, including telehealth. The consent management system captures, stores, and manages patient consent for treatment, telehealth-specific disclosures, recording, data sharing, research participation, and privacy practices. Each consent must include a versioned document, e-signature, timestamp, and IP address for legal defensibility.
Consent Types
| Consent Type | When Required | Validity | Revocable |
|---|---|---|---|
| Informed Treatment Consent | Before first visit | 1 year | Yes, at any time |
| Telehealth Disclosure | Before first virtual visit | 1 year | Yes |
| Privacy Practices (NPP) | First visit + annual update | 1 year | Acknowledgment only |
| Session Recording Consent | Before each recording | Per session | Yes, before recording starts |
| Data Sharing Consent | Optional, per data type | Until revoked | Yes, at any time |
| Research Participation | Before enrollment | Duration of study | Yes, without penalty |
| Minor Patient Consent | For minors (varies by state) | Varies | Parent/guardian |
C#
public class ConsentManagementService
{
private readonly IConsentRepository _repo;
private readonly IDocumentVersioning _versioning;
private readonly IAuditLogger _auditLogger;
public async Task<ConsentCaptureResult>
CaptureConsentAsync(CaptureConsentRequest req)
{
// Get latest version of consent document
var latestVersion = await _versioning
.GetLatestVersionAsync(req.ConsentType);
if (latestVersion == null)
throw new NotFoundException(
$"No consent document for {req.ConsentType}");
// Check if patient already has this consent
var existing = await _repo
.GetActiveConsentAsync(
req.PatientId, req.ConsentType);
if (existing != null
&& existing.Version == latestVersion.Version)
{
return ConsentCaptureResult
.AlreadyConsented(existing);
}
// Create consent record
var consent = new PatientConsent
{
Id = Guid.NewGuid(),
PatientId = req.PatientId,
ConsentType = req.ConsentType,
Version = latestVersion.Version,
DocumentContentHash =
latestVersion.ContentHash,
DocumentTitle = latestVersion.Title,
SignedAt = DateTime.UtcNow,
IPAddress = req.IPAddress,
UserAgent = req.UserAgent,
Method = ConsentMethod.Electronic,
WitnessId = req.WitnessId,
ExpiresAt = DateTime.UtcNow
.AddYears(1),
// Store the signed document snapshot
SignedDocumentSnapshot =
latestVersion.Content
};
// If patient previously had consent, link to prior
if (existing != null)
{
consent.PreviousConsentId = existing.Id;
consent.ChangeReason = "Renewal";
}
await _repo.SaveConsentAsync(consent);
// If previous consent existed, supersede it
if (existing != null)
{
existing.SupersededAt = DateTime.UtcNow;
existing.SupersededBy = consent.Id;
await _repo.UpdateConsentAsync(existing);
}
await _auditLogger.LogAsync(new AuditEvent
{
Action = "consent.capture",
ResourceId = consent.Id,
PatientId = req.PatientId,
Details = new
{
ConsentType = req.ConsentType,
Version = consent.Version,
Method = ConsentMethod.Electronic
}
});
return ConsentCaptureResult.Captured(consent);
}
public async Task RevokeConsentAsync(
Guid patientId, ConsentType type,
string reason)
{
var consent = await _repo
.GetActiveConsentAsync(patientId, type);
if (consent == null)
throw new NotFoundException(
"No active consent found");
consent.RevokedAt = DateTime.UtcNow;
consent.RevocationReason = reason;
await _repo.UpdateConsentAsync(consent);
// Trigger downstream effects
if (type == ConsentType.Recording)
{
// Stop any active recordings
await StopActiveRecordingsAsync(patientId);
}
if (type == ConsentType.DataSharing)
{
// Revoke data sharing agreements
await RevokeDataSharingAsync(patientId);
}
await _auditLogger.LogAsync(new AuditEvent
{
Action = "consent.revoke",
ResourceId = consent.Id,
PatientId = patientId,
Details = new
{
ConsentType = type,
Reason = reason
}
});
}
public async Task<ConsentStatusResponse>
GetConsentStatusAsync(Guid patientId)
{
var consents = await _repo
.GetAllConsentsAsync(patientId);
return new ConsentStatusResponse
{
PatientId = patientId,
Consents = Enum.GetValues<ConsentType>()
.Select(type =>
{
var active = consents
.Where(c => c.ConsentType == type
&& c.RevokedAt == null
&& c.ExpiresAt > DateTime.UtcNow)
.OrderByDescending(
c => c.SignedAt)
.FirstOrDefault();
return new ConsentStatusItem
{
Type = type,
IsConsented = active != null,
ConsentedAt =
active?.SignedAt,
ExpiresAt =
active?.ExpiresAt,
Version = active?.Version,
DaysUntilExpiry =
active?.ExpiresAt
.Subtract(DateTime.UtcNow)
.Days
};
}).ToList()
};
}
}
27. Platform Scalability & Performance
Telehealth platforms face unique scaling challenges: video sessions consume orders of magnitude more resources than typical API requests, appointment booking creates thundering-herd patterns at the top of each hour, and public health emergencies can cause 10-50x demand spikes. This section covers the scaling strategies for each critical subsystem.
Scaling Video Infrastructure
Each 1:1 HD video session requires approximately 2-3 Mbps of bandwidth per participant plus SFU forwarding capacity. For 100,000 simultaneous sessions, the SFU infrastructure must handle roughly 200-300 Gbps of aggregate throughput. We achieve this through:
- Geographic Distribution: SFU clusters in US-East, US-West, EU-West, and APAC regions. DNS-based routing sends participants to the nearest cluster. Cross-region fallback is available if a regional cluster reaches capacity.
- Horizontal Scaling: SFU nodes are stateless with respect to room assignment. New nodes can be added to a cluster within 2 minutes. Load balancers distribute new sessions based on current node utilization.
- Adaptive Quality: During capacity constraints, the system can downgrade default video quality from 720p to 480p, reducing bandwidth by 56% per session. Audio quality is always prioritized over video.
- Session Multiplexing: For group calls (3+ participants), SFU selectivity reduces per-participant bandwidth. A 5-party call requires 5 publish streams but each participant only subscribes to 4, with the SFU handling intelligent forwarding.
Scaling the Scheduling System
Appointment booking creates predictable traffic spikes. When a popular provider opens their schedule for next week, hundreds of patients may attempt to book the same slots simultaneously. Our approach:
- Optimistic Locking with Row-Level Constraints: PostgreSQL advisory locks prevent double-booking. The appointment_slots table has a unique constraint on (provider_id, slot_start) WHERE status = 'booked'. Two concurrent INSERTs for the same slot will have one fail with a unique violation, which the application catches and returns a "slot no longer available" message.
- Distributed Slot Hold Cache: Redis holds a 5-minute TTL lock on a slot when a patient begins the booking flow. This prevents other patients from even attempting to book the same slot, reducing failed transactions by 90%.
- Eventual Consistency for Availability Display: The public availability view is cached in Redis with a 30-second TTL. This means a slot might show as "available" for up to 30 seconds after being booked. The actual booking flow always checks the source-of-truth database.
Scaling Clinical Data Access
Clinical data queries (patient summary, medication list, allergy list) must be fast during live visits but involve complex joins across multiple FHIR resources. The strategy:
- Patient Summary Cache: When a patient checks in for a visit, the system pre-fetches and caches their clinical summary (demographics, active medications, allergies, recent vitals, problem list) in Redis with a 1-hour TTL. This reduces in-call data lookups from 500ms to under 5ms.
- CQRS for Clinical Notes: The write path (creating/editing notes) goes to MongoDB. The read path (searching, listing, viewing) queries a PostgreSQL materialized view that is updated via Kafka events within 5 seconds.
- Vitals Time-Series Partitioning: TimescaleDB automatically partitions vitals data by time. Queries for "last 24 hours" hit only the current partition. Data older than 90 days is compressed and moved to a cold partition, reducing storage costs by 90%.
28. Interview Q&A Deep Dive
Q1: How do you handle a video call dropping mid-visit?
Answer: Multi-layered reconnection: (1) Detect via WebRTC oniceconnectionstatechange. (2) Attempt reconnection within 5 seconds via backup TURN server. (3) Fall back to audio-only if video fails. (4) Offer PSTN dial-in via Twilio if internet fails entirely. (5) Provider sees countdown timer during reconnection. (6) If no reconnect in 2 minutes, session marked as interrupted with partial billing. All attempts audit-logged.
Q2: How do you ensure HIPAA compliance with Kafka?
Answer: PHI never in Kafka payloads or topic names. Field-level encryption before publishing. Audit.log topic carries only metadata with sanitized details. Dedicated cluster for clinical topics with strict ACLs. Encrypted at rest via MSK/KMS. Retention aligned with HIPAA (7 years audit, 10 years clinical). Avro schema enforcement prevents accidental PHI fields.
Q3: How do you handle provider licensing across state lines?
Answer: Provider_licenses table with state, number, type, expiry. Pre-booking validation checks: (1) Active license in patient's current state, (2) License not expired, (3) Visit type permitted under state telehealth laws. Patient's state tracked at check-in. Failed validation prevents booking and suggests licensed alternatives. Cached license data with 100ms timeout.
Q4: How do you handle demand surges during public health emergencies?
Answer: Elastic scaling: (1) SFU auto-scales via LiveKit Cloud. (2) Waiting room uses Redis Cluster for horizontal sharding. (3) Scheduling switches to virtual queue mode. (4) AI triage becomes mandatory. (5) Visit duration reduced for follow-ups. (6) Partner provider sharing API. (7) Predictive auto-scaling based on historical surge patterns.
Q5: How does e-prescribing for controlled substances (EPCS) work?
Answer: (1) DEA registration verified at credentialing. (2) Schedule II-V requires 2FA (hardware token/biometric). (3) PDMP query before transmission. (4) NCPDP SCRIPT with PKI digital signature via Surescripts. (5) Dedicated audit entry with DEA number, verification method, PDMP results. (6) State-specific rules enforced (90-day supply limits, in-person visit requirements).
Q6: Explain the lab order data flow.
Answer: (1) Provider orders in clinical notes, mapped to LOINC codes. (2) Lab Orders Service validates authority, insurance coverage, nearby draw centers. (3) Transmitted via HL7 ORM or FHIR ServiceRequest. (4) Patient receives notification with locations and prep instructions. (5) Lab processes sample. (6) Results via HL7 ORU or FHIR Observation. (7) Abnormal values flagged, criticals trigger immediate alert. (8) Provider reviews and documents in clinical note. (9) Results shared with patient via portal. (10) Encounter record sent to PCP via FHIR DocumentReference.
Q7: How do you prevent the waiting room from becoming a bottleneck?
Answer: (1) Real-time queue metrics (average wait, patients per provider, abandonment rate). (2) Predictive wait times based on current provider pace. (3) Dynamic provider routing during surges (activate on-call providers when queue > threshold). (4) Self-service rescheduling for estimated waits > 30 minutes. (5) Triage diverts non-urgent to async messaging. (6) Auto-scaling during peak hours (evening flu season). (7) Provider dashboard shows queue depth, enabling voluntary overtime.
Q8: How do you handle PHI in analytics without violating HIPAA?
Answer: Two-tier analytics pipeline: (1) Real-time operational analytics use de-identified data with k-anonymity (k>=5) applied at the stream processor. (2) Business analytics use fully de-identified datasets following Safe Harbor method (18 PHI identifiers removed). (3) A separate data science environment processes only synthetic or formally de-identified data. (4) All analytics access goes through a PHI-free data mart. (5) Differential privacy applied to aggregate reporting. (6) Regular PHI leak scans on all dashboards and reports.
Q9: How do you handle insurance claim denials and appeals?
Answer: Automated denial management: (1) Parse ERA/835 denial reason codes. (2) Classify denials (eligibility, coding, prior auth, medical necessity). (3) Auto-generate appeal letters with supporting documentation from clinical notes. (4) Route to billing specialist for review. (5) Track appeal deadlines and escalate. (6) Learn from denial patterns to prevent future claims (e.g., if a payer consistently denies a CPT code, flag during superbill creation). (7) Analytics dashboard shows denial rates by payer, provider, and diagnosis.
Q10: Design considerations for accessibility (ADA/Section 508)?
Answer: (1) WCAG 2.1 AA compliance across all patient-facing interfaces. (2) Screen reader support for waiting room, video calls, and forms. (3) Closed captions during video visits (real-time speech-to-text). (4) High contrast mode and adjustable font sizes. (5) Keyboard navigation for all workflows. (6) American Sign Language (ASL) interpreter on-demand. (7) Multi-language support with provider language matching. (8) Accessible consent forms with plain language versions. (9) Regular accessibility audits with disabled users. (10) Alternative text for all medical images shared in-platform.
Pre-Interview Checklist
- Understand WebRTC architecture (SFU vs mesh, ICE, TURN/STUN)
- Know HIPAA compliance requirements (BAA, encryption, audit, minimum necessary)
- Design HIPAA-compliant audit logging with PHI sanitization
- Understand HL7 FHIR R4 resources and SMART on FHIR authentication
- Know NCPDP SCRIPT standard for e-prescribing and EPCS requirements
- Design a real-time waiting room queue with Redis sorted sets
- Explain graceful degradation for video (adaptive bitrate, audio-only, PSTN fallback)
- Understand insurance eligibility verification (EDI 270/271) and claims (837/835)
- Discuss provider credentialing workflow and state licensing validation
- Know SOAP note structure and ICD-10/CPT coding workflows
- Explain consent management for treatment, recording, and data sharing
- Describe scaling strategies for video (SFU clusters, adaptive quality, geographic routing)
- Discuss thundering herd mitigation for appointment booking (optimistic locking, slot holds)
- Know the difference between CQRS for clinical notes write vs read paths
- Explain how to handle provider panel management and state-specific telehealth regulations