system-design46 min read

How to Design a Telemedicine & Virtual Health Platform — A Senior+ Guide | Ayodhyya

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

Senior+ System Design Guide 10,000+ Words 28 Deep-Dive Sections C# · Mermaid · HL7 FHIR · WebRTC

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.

Why This Design Is Unique: Telehealth platforms must simultaneously satisfy three audiences: patients who expect consumer-grade UX, providers who need clinical-grade reliability, and regulators who mandate healthcare-grade compliance. Every architectural decision must balance all three.

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

ModuleFeaturesPriority
Video Consultation1:1 HD video/audio, screen sharing, adaptive bitrate, audio-only fallbackP0
Appointment SchedulingProvider availability, patient booking, recurring appointments, reminders, timezone handlingP0
Waiting RoomVirtual check-in, queue position, estimated wait time, provider notificationsP0
Provider DirectorySearch by specialty, availability, insurance, language, rating; provider profilesP0
EHR IntegrationFHIR R4 read/write, patient demographics, clinical documents, allergies, medicationsP0
PrescriptionsE-prescribing via NCPDP SCRIPT, pharmacy routing, prior auth workflowsP0
Lab Orders & ResultsLab order creation, result delivery, abnormal result flagging, provider reviewP1
Insurance VerificationReal-time eligibility checks, copay estimation, prior authorizationP1
Payment ProcessingCopay collection, split billing, superbills, sliding scale feesP0
Secure MessagingAsync chat, file attachment (images, PDFs), read receipts, provider queueP1
Multi-Party Calls3+ participants, specialist referral mid-call, interpreter supportP1
RecordingSession recording with explicit consent, secure storage, playbackP2
Clinical NotesSOAP note templates, auto-save, ICD-10/CPT coding suggestions, sign-off workflowP0
Billing & CodingCPT/ICD-10 code management, claim generation, ERA/835 processingP1
Vitals MonitoringConnected device data ingestion (BP, SpO2, glucose), real-time display, alertingP2
AI TriageSymptom checker, acuity scoring, routing logic, escalation workflowsP2
Consent ManagementInformed consent capture, e-signatures, consent versioning, audit trailP0
Provider CredentialingLicense verification, DEA registration, board certification, privilegingP1

Non-Functional Requirements

RequirementTargetNotes
Availability99.95% (video 99.99%)Multi-region active-active for video
Video Latency< 200ms end-to-endWebRTC with edge TURN servers
API Latencyp99 < 500msScheduling, directory, clinical APIs
Concurrent Users100K video sessionsScalable media servers (SFU)
EncryptionE2E video, AES-256 rest, TLS 1.3 transitHIPAA-mandatory standards
Audit Logging100% PHI access eventsImmutable log, 7-year retention
Disaster RecoveryRPO < 1 min, RTO < 15 minHot standby, automatic failover
ComplianceHIPAA, SOC 2 Type II, HITRUSTAnnual audits, pen testing
Critical Constraint: Under HIPAA, Protected Health Information (PHI) must never appear in logs, error messages, or analytics pipelines. Every service boundary must enforce the Minimum Necessary standard. A single PHI leak can trigger multi-million dollar penalties.

3. High-Level Architecture Overview

graph TB subgraph Client Layer PWA[Web App - React PWA] Mobile[Mobile Apps - iOS Android] ProviderApp[Provider Portal - Desktop] end subgraph Edge and Security CDN[CDN / WAF] APIGateway[API Gateway - Kong] Auth[Auth Service - OAuth2 + SMART on FHIR] end subgraph Core Services US[User Service] SS[Scheduling Service] VS[Video Service - WebRTC SFU] PS[Provider Service] WS[Waiting Room Service] MS[Messaging Service] CS[Consent Service] end subgraph Clinical Services EHR[EHR Integration - FHIR R4] PRESC[Prescription Service - NCPDP] LABS[Lab Orders Service] NOTES[Clinical Notes Service] VITALS[Vitals Ingestion Service] TRIAGE[AI Triage Service] end subgraph Financial Services INS[Insurance Verification] PAY[Payment Service - Stripe] BILL[Billing and Coding] CREDS[Credentialing Service] end subgraph Data Layer PG[PostgreSQL] Redis[Redis Cluster] Mongo[MongoDB] S3[S3 - Recordings and Files] Kafka[Kafka - Event Streaming] ES[Elasticsearch] end subgraph External FHIR_EXT[External EHRs] Pharm[Pharmacy - Surescripts] LabExt[Labs - Quest and LabCorp] InsExt[Insurance Clearinghouses] TURN[TURN/SFU Infrastructure] end PWA --> CDN Mobile --> CDN ProviderApp --> CDN CDN --> APIGateway APIGateway --> Auth Auth --> US US --> PG SS --> Kafka VS --> TURN EHR --> FHIR_EXT PRESC --> Pharm LABS --> LabExt INS --> InsExt NOTES --> Mongo VITALS --> Kafka MS --> Redis Kafka --> ES

Architecture Principles

  1. 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.
  2. 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.
  3. 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.
  4. 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

PatternUse CasesImplementation
Synchronous REST/gRPCAPI queries, authentication, lookupsgRPC internal, REST external
Async Events (Kafka)Clinical events, billing, auditAvro schema, exactly-once
WebSocket / SSEReal-time vitals, waiting room, chatRedis pub/sub fan-out
WebRTC Data ChannelsIn-call signaling, file transferCustom 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}";
}
Storage Selection Rationale: PostgreSQL for ACID transactional data. MongoDB for flexible-schema clinical notes. Redis for sub-millisecond real-time state. S3 for binary assets. Elasticsearch for full-text search. Kafka for event streaming connecting all systems.

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

sequenceDiagram participant Patient participant SigServer as Signaling Server participant SFU as Media Server SFU participant Provider Patient->>SigServer: Join room (offer SDP) SigServer->>SFU: Create/select room Provider->>SigServer: Join room (offer SDP) SigServer->>SFU: Connect provider peer loop ICE Connectivity Patient->>SFU: STUN/TURN binding Provider->>SFU: STUN/TURN binding end Patient->>SFU: Publish media stream SFU->>Provider: Subscribe to patient stream Provider->>SFU: Publish media stream SFU->>Patient: Subscribe to provider stream loop Adaptive Bitrate SFU-->>SFU: Monitor packet loss and jitter SFU-->>Patient: Quality notifications Patient->>SFU: Adjust resolution and bitrate end

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;
    }
}
TURN Server Strategy: Deploy TURN servers in every major cloud region using geo-routing DNS. For 90%+ of calls, WebRTC ICE will find a direct peer connection. TURN servers handle the 10-15% of calls behind symmetric NATs or strict firewalls.

Graceful Degradation: Audio-Only Fallback

ConditionActionUser Experience
Bandwidth > 2.5 Mbps, loss < 0.5%HD 1080p @ 30fpsFull HD video
Bandwidth 1-2.5 Mbps, loss < 2%720p @ 30fpsClear HD video
Bandwidth 0.5-1 Mbps, loss < 5%480p @ 24fpsStandard quality
Bandwidth 0.2-0.5 Mbps, loss < 10%360p @ 15fpsLower quality
Bandwidth < 0.2 MbpsAudio onlyAudio call with photo
Total connection failurePSTN dial-inPhone 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.

sequenceDiagram participant Patient participant WR as Waiting Room Service participant Queue as Redis Sorted Set participant Notify as Notification Service participant Provider Patient->>WR: checkIn(appointmentId) WR->>Queue: ZADD provider:id score=ts member=pid WR-->>Patient: position=3 estWait=15 min loop Every 30 seconds WR->>Queue: ZRANK provider:id patientId WR-->>Patient: SSE position update end Provider->>WR: getNextPatient() WR->>Queue: ZPOPMIN provider:id WR->>Notify: Alert patient to join Notify-->>Patient: Push notification Patient->>WR: joinSession() WR-->>Patient: videoSessionUrl

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.

graph LR A[Patient Selects Provider] --> B[View Available Slots] B --> C{Slot Available?} C -->|Yes| D[Hold Slot 5 min] C -->|No| E[Suggest Alternatives] D --> F[Patient Confirms] F --> G[Create Appointment] G --> H[Send Confirmation] G --> I[Block Slot] G --> J[Trigger Insurance Verify] D --> K{Hold Expires?} K -->|Yes| L[Release Slot] K -->|No| F

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

TimingChannelContent
24 hours beforeSMS + EmailAppointment details, prep instructions
2 hours beforeSMS + Push"Your visit is in 2 hours"
15 minutes beforePush + Email"Time to check in!"
At scheduled timePush"Your provider is ready"
No-show (15 min)SMS"We missed you. Reschedule"
Timezone Handling: All timestamps stored in UTC. Provider schedules in local timezone. DST transitions handled correctly.

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

FactorWeightLogic
Current availability35%Provider online and free?
Triage acuity25%Higher acuity to urgent provider
Specialty match20%Exact specialty vs general
Patient history10%Previously seen?
Language5%Shared language
Load balancing5%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

graph TB subgraph Our FHIR Server PatientRes[Patient Resource] PractitionerRes[Practitioner Resource] EncounterRes[Encounter Resource] ObservationRes[Observation - Vitals] MedReq[MedicationRequest] ServiceReq[ServiceRequest - Labs] DocRef[DocumentReference] CondRes[Condition - Diagnoses] end subgraph External EHR SMART[SMART on FHIR OAuth 2.0] BulkFHIR[Bulk FHIR Export] end SMART --> PatientRes SMART --> EncounterRes BulkFHIR --> ObservationRes

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);
    }
}
FHIR Integration Strategy: Build a thin adapter layer translating between our domain model and FHIR resources. This decouples core services from the quirks of each EHR's FHIR implementation. Epic behaves differently from Cerner in pagination, error handling, and search parameters.

Key FHIR Resources

FHIR ResourcePurposeService
PatientDemographics, contact, insuranceUser, EHR Integration
PractitionerProvider credentialsProvider Service
EncounterVisit recordEHR Integration
ObservationVitals, lab resultsVitals, Lab Service
MedicationRequestPrescriptionsPrescription Service
ServiceRequestLab/imaging ordersLab Service
DocumentReferenceClinical notesNotes Service
ConditionDiagnosesNotes Service
AllergyIntoleranceAllergiesEHR Integration
CoverageInsuranceInsurance Service
ClaimBillingBilling 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.

sequenceDiagram participant Provider participant Presc as Prescription Service participant CDS as Clinical Decision Support participant Surescripts participant Pharmacy Provider->>Presc: Create prescription Presc->>CDS: Drug interaction check CDS-->>Presc: Interactions result alt Major Interaction Presc-->>Provider: Warning Provider->>Presc: Override with justification end Presc->>Presc: Validate authority alt Controlled Substance EPCS Presc->>Provider: Require 2FA + DEA token Provider->>Presc: Authenticate end Presc->>Surescripts: Transmit (NCPDP SCRIPT) Surescripts->>Pharmacy: Forward prescription Pharmacy-->>Surescripts: Acceptance Surescripts-->>Presc: Status Presc-->>Provider: Sent confirmation
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);
    }
}
EPCS Compliance: Requires 2FA (hardware token or biometric), DEA registration verification, and immutable audit trails. Schedule II-V prescriptions need special handling. State-specific rules limit supply and require in-person visits for initial prescriptions.

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

StatusDescriptionActor
DraftedProvider creates order during visitProvider
SignedProvider signs and transmitsProvider
TransmittedSent to lab network (HL7 ORM)System
ReceivedLab acknowledgesLab
In ProgressSample collected at draw centerPatient + Lab
Results ReadyAnalysis completeLab
Provider ReviewedProvider documents reviewProvider
Patient NotifiedResults shared via portalSystem
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.

graph LR A[Patient Check-In] --> B[Insurance Card OCR] B --> C[Eligibility Request EDI 270] C --> D[Clearinghouse] D --> E{Eligible?} E -->|Yes| F[Copay and Deductible] E -->|No| G[Self-Pay Flow] E -->|Error| H[Retry Manual] F --> I[Notify Patient] I --> J[Collect Copay]
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.

graph TB subgraph Point of Service Copay[Copay via Stripe] SelfPay[Self-Pay] end subgraph Claims Pipeline Superbill[Superbill Generation] Claim837[Claim EDI 837P] Submit[Submit to Clearinghouse] Adjudication[Insurance Adjudication] ERA[ERA/835 Processing] end subgraph Reconciliation Balance[Balance Calculation] PatientBill[Patient Statement] end Copay --> Superbill SelfPay --> Superbill Superbill --> Claim837 Claim837 --> Submit Submit --> Adjudication Adjudication --> ERA ERA --> Balance Balance --> PatientBill
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 TypeMax SizeFormatsRetention
Medical Images50 MBDICOM, JPEG, PNG10 years
Lab Results10 MBPDF10 years
Insurance Cards5 MBJPEG, PNG, PDFCoverage + 2 years
Consent Forms5 MBPDF10 years
General25 MBPDF, JPEG, PNG7 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.

graph TB subgraph Multi-Party SFU SFU[SFU Cluster] Router[Track Selection Router] end subgraph Participants Patient[Patient] Provider[Provider] Specialist[Specialist] Interpreter[Interpreter] Family[Family Member] end Patient --> SFU Provider --> SFU Specialist --> SFU Interpreter --> SFU Family --> SFU SFU --> Router
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);
    }
}
Interpreter Support: Medical interpreters join as third participants. The SFU delivers both audio streams to the interpreter. A language toggle in the UI allows switching between interpreter translation and direct audio. This requires per-subscription audio track routing on the SFU.

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.

sequenceDiagram participant Patient participant Provider participant RecService as Recording Service participant ConsentService participant Storage as Encrypted Storage Provider->>RecService: Start recording RecService->>ConsentService: Check consent alt Consent needed ConsentService-->>Patient: Recording consent form Patient->>ConsentService: Accept ConsentService-->>RecService: Confirmed end alt Consent refused Patient->>ConsentService: Refuse ConsentService-->>Provider: Declined end RecService->>RecService: Start SFU recording loop During Session RecService->>Storage: Encrypted chunks end Provider->>RecService: Stop recording RecService->>Storage: Final with metadata
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

SectionContentData Type
SubjectiveChief complaint, HPI, ROSFree text + structured
ObjectiveVitals, exam findingsNumeric + free text
AssessmentDiagnoses, ICD-10 codesStructured + free text
PlanMeds, orders, referralsStructured + 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.

graph TB Input[Patient Symptoms] --> NLP[NLP Extraction] NLP --> Match[Clinical Rule Engine] Match --> Score[Acuity Score 1-5] Score --> Route[Routing Engine] Route --> L1[Level 1 - Emergency] Route --> L2[Level 2 - Urgent] Route --> L3[Level 3 - Semi-Urgent] Route --> L4[Level 4 - Routine] Route --> L5[Level 5 - Non-Urgent]
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
        };
    }
}
Medical Disclaimer: AI triage is a decision-support tool, not a replacement for clinical judgment. For red-flag symptoms (chest pain, breathing difficulty, stroke signs, severe bleeding), immediately display emergency instructions and offer 911 connection.

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.

graph LR Device[BLE Device] --> Mobile[Mobile App] Mobile --> Ingestion[Vitals API] Ingestion --> Kafka[Kafka vitals.raw] Kafka --> Processor[Stream Processor] Processor --> Store[TimescaleDB] Processor --> Alert[Alert Engine] Processor --> Dashboard[Provider Dashboard]
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

DeviceMetricsNormalCritical
BP CuffSystolic/Diastolic90-140/60-90>180/120
Pulse OxSpO2, HR95-100%, 60-100SpO2 <90%
GlucometerBlood Glucose70-140 mg/dL>400 or <54
ThermometerTemperature97-99F>104F
Smart ScaleWeight, BMIBaseline +/-5%>5 lbs/day
SmartwatchHR, HRV, StepsBaselineAfib, 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.

graph TB subgraph Perimeter WAF[AWS WAF] DDoS[DDoS Shield] RateLimit[Rate Limiting] end subgraph Auth OAuth[OAuth 2.0 OIDC] MFA[Multi-Factor Auth] RBAC[RBAC + ABAC] end subgraph Encryption TLS[TLS 1.3] AES[AES-256] E2E[E2E Video] KMS[AWS KMS] end subgraph Monitoring AuditLog[Immutable Audit Log] SIEM[SIEM] PHI[PHI Monitoring] end WAF --> OAuth --> MFA --> RBAC TLS --> AES --> E2E --> KMS AuditLog --> SIEM --> PHI
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

RequirementImplementationStatus
Encryption at restAES-256 via KMSDone
Encryption in transitTLS 1.3 everywhereDone
Access controlsRBAC + ABACDone
Audit logging100% PHI access, 7yrDone
BAAsAll vendorsDone
Breach notification60-day workflowDone
Minimum necessaryField-level per roleDone
De-identificationSafe HarborDone
DRRPO <1min, RTO <15minDone
TrainingAnnual + phishingDone

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);
    }
}
BAAs Required: Every third-party service touching PHI needs a signed BAA: AWS, Twilio, Stripe, Bedrock. No BAA while handling PHI = HIPAA violation.

21. API Design

RESTful APIs with OpenAPI 3.1 specs. Internal gRPC. External REST/JSON.

Core Endpoints

MethodEndpointDescriptionAuth
GET/api/v1/providers/searchSearch providersJWT
GET/api/v1/providers/{id}/slotsAvailable slotsJWT
POST/api/v1/appointmentsCreate appointmentJWT
POST/api/v1/waiting-room/check-inCheck inJWT
POST/api/v1/video/sessionsCreate sessionJWT+Role
GET/api/v1/patients/{id}/ehr/summaryFHIR summaryJWT+Scope
POST/api/v1/prescriptionsCreate RxJWT+Role
POST/api/v1/lab-ordersCreate lab orderJWT+Role
POST/api/v1/insurance/verifyVerify eligibilityJWT
POST/api/v1/paymentsProcess paymentJWT
POST/api/v1/messagesSend messageJWT
POST/api/v1/clinical-notesCreate noteJWT+Role
POST/api/v1/clinical-notes/{id}/signSign noteJWT+Role
POST/api/v1/triage/assessAI triageJWT
POST/api/v1/vitalsIngest vitalsDevice
POST/api/v1/consentCapture consentJWT
GET/api/v1/audit/patient/{id}PHI audit logAdmin

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

CategoryLimitWindow
Search601 min
Booking101 min
Payment51 min
Video1001 min
Messaging301 min
Vitals1201 min
Triage55 min

22. Cost Estimation

HIPAA-compliant infrastructure is expensive due to encryption, media servers, compliance, and HA.

Monthly (10K Patients, 50 Providers)

CategoryServiceCost
ComputeEKS 8 nodes$2,800
DBRDS PostgreSQL Multi-AZ$1,200
DBDocumentDB$600
CacheElastiCache Redis$900
MQMSK Kafka$1,200
StorageS3$120
MediaLiveKit Cloud$5,000
CDNCloudFront + WAF$300
SearchOpenSearch$800
RxSurescripts$500
InsuranceClearinghouse$400
CommsTwilio$600
EmailSendGrid$100
AIBedrock$400
PaymentsStripe2.9%+$0.30
MonitoringCloudWatch+Datadog$500
SecurityKMS+GuardDuty$400
ComplianceVanta$1,500
CI/CDTooling$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

LayerTypeCoverageTools
UnitLogic, calculations90%+xUnit, Moq
IntegrationService-to-service80%+Testcontainers
ContractAPI contracts, FHIR100%Pact, FHIR validator
E2EUser journeysCriticalPlaywright
LoadConcurrent sessionsPeak+2xk6
SecuritySAST, DAST, pen test0 criticalSonarQube, ZAP
ComplianceHIPAA controlsAllVanta + 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

StepVerificationSourceSLA
1Medical school graduationAMA Physician Masterfile or medical school5 business days
2Residency/fellowship completionTraining institution or ABMS5 business days
3State medical licenseState medical boardReal-time API where available
4DEA registrationDEA Active Registrants databaseReal-time API
5Board certificationABMS or AOAReal-time API
6NPI numberNPPES NPI RegistryReal-time API
7Malpractice historyNational Practitioner Data Bank (NPDB)10 business days
8Criminal backgroundFBI/state background check15 business days
9OIG/SAM exclusion checkOIG LEIE and SAM.govReal-time
10Work history (5 years)Previous employers15 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");
    }
}
Continuous Monitoring: Beyond initial credentialing, the system runs continuous monitoring checks: monthly OIG/SAM exclusion scans, daily license expiration alerts, and real-time NPDB query triggers when malpractice reports are filed. A provider who loses their license must be removed from the platform within 24 hours.

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%.
Capacity Planning: The platform maintains a capacity model that maps: (1) provider count to scheduling throughput, (2) concurrent sessions to SFU node count, (3) patient count to database connection pool size, and (4) event throughput to Kafka partition count. Auto-scaling policies use these models to pre-scale infrastructure 15 minutes before predicted demand peaks (e.g., Monday mornings, flu season onset).

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

Telemedicine & Virtual Health Platform — Senior+ Guide | Ayodhyya