SYSTEM_DESIGN

System Design: Patient Portal

Design a secure HIPAA-compliant patient portal enabling health record access, secure provider messaging, prescription management, bill pay, and health data sharing via HL7 FHIR APIs for millions of patients.

16 min readUpdated Jan 15, 2025
system-designpatient-portalhealthcarehipaahl7-fhir

Requirements

Functional Requirements:

  • Patients can view their medical records including lab results, medications, allergies, immunizations, and visit summaries via HL7 FHIR APIs
  • Secure messaging between patients and their care team with attachment support
  • Prescription refill requests and medication list management with pharmacy selection
  • Online bill pay and insurance claim status tracking with explanation of benefits (EOB) display
  • Proxy access for caregivers managing family members' health (pediatric, elderly, dependents)
  • Health data export and sharing via FHIR Bulk Data and patient-directed exchange (TEFCA compliance)

Non-Functional Requirements:

  • Support 5M registered patients with 500K daily active users
  • Page load time under 2 seconds for health record views
  • HIPAA compliance: encryption at rest (AES-256) and in transit (TLS 1.3), MFA for authentication
  • ONC 21st Century Cures Act compliance: no information blocking, standardized FHIR API access
  • 99.9% availability with planned maintenance windows outside peak hours (6 AM - 10 PM)

Scale Estimation

With 500K DAU and an average of 8 page views per session, the portal serves 4M page views/day = 46 requests/sec average, peaking at 200 requests/sec during weekday evenings (6-9 PM) when patients check newly posted lab results. Lab result notifications drive traffic spikes: when a health system releases lab results at 5 PM, 50,000 patients may check results within 30 minutes = 1,600 requests/sec burst. Secure messaging: 200K messages/day with 5KB average payload = 1GB/day. Prescription refill requests: 30K/day. Bill pay transactions: 20K/day averaging $150. FHIR API calls from third-party apps (patient-authorized): 100K/day via SMART on FHIR.

High-Level Architecture

The patient portal is a React-based SPA (Single Page Application) backed by a BFF (Backend for Frontend) layer and domain microservices. The BFF (Node.js) aggregates data from multiple backend services into patient-friendly response payloads, handles session management, and implements the presentation logic (e.g., flagging abnormal lab results, calculating medication adherence scores). Authentication uses OAuth 2.0 with PKCE flow via an Identity Provider (Keycloak) supporting MFA (TOTP, SMS, WebAuthn), SSO with health system employee portals, and SMART on FHIR authorization for third-party app access.

Backend services are organized by domain: Health Records Service (reads from the EHR's FHIR server and caches patient data), Messaging Service (secure async communication), Pharmacy Service (refill requests routed to pharmacy systems via NCPDP SCRIPT), Billing Service (integrates with revenue cycle management systems), and Identity & Access Service (manages patient accounts, proxy relationships, and consent directives). All services communicate via REST over internal mTLS. The Health Records Service does not store its own copy of clinical data — it acts as a FHIR client to the upstream EHR system, with a Redis cache layer to reduce load on the EHR.

All PHI is encrypted at rest using AES-256 via AWS KMS with separate keys per data classification (clinical records, messages, billing). Audit logging captures every patient data access event and streams to a SIEM (Splunk) for real-time monitoring. A consent management service tracks patient preferences for data sharing and applies access policies before any data is returned.

Core Components

Health Records Aggregation Service

This service provides a unified patient health record view by aggregating data from the EHR's FHIR R4 server. When a patient opens their health summary, the service makes parallel FHIR API calls: Patient/$everything for the encounter history, Observation?category=laboratory for lab results, MedicationRequest for active medications, Condition for diagnoses, and Immunization for vaccine records. Results are cached in Redis with patient-specific keys and a 15-minute TTL (cache invalidation is triggered by EHR subscription notifications via FHIR Subscriptions). Lab results include a patient-friendly interpretation layer: the service compares Observation values against the reference range and flags abnormal results with plain-language explanations (e.g., "Your cholesterol is above the recommended range"). The service enforces ONC information blocking rules — all data available to the provider is available to the patient with limited exceptions (safety, privacy).

Proxy Access & Consent Management

The Proxy Access system handles the complex authorization model where caregivers may access others' health records. Proxy relationships are stored in PostgreSQL: proxy_grants (grantor_patient_id, grantee_user_id, relationship_type PARENT/GUARDIAN/CAREGIVER/POWER_OF_ATTORNEY, access_scope, valid_from, valid_to, verification_status). Age-based rules automatically manage pediatric access: parents have full access to children's records until age 12, limited access from 12-18 (behavioral health and reproductive health restricted per state law), and no default access after 18. The Consent Management service stores patient consent directives as FHIR Consent resources, evaluated at every data access point. Consent directives can restrict specific data categories (e.g., "do not share substance abuse records with my spouse proxy") and are enforced by a Policy Decision Point (OPA — Open Policy Agent) queried by every service before returning PHI.

Notification & Engagement Engine

The Notification Engine drives patient engagement by delivering timely, actionable notifications. When lab results are posted, the EHR publishes a FHIR Subscription notification to the portal's webhook endpoint. The Notification Engine processes the event, determines the appropriate channels (push, SMS, email) based on patient preferences, and sends personalized messages: "Your lab results from Dr. Smith are now available. Tap to view." Notifications are sent via a priority queue — abnormal lab results and urgent messages are sent immediately; routine notifications (appointment reminders, wellness tips) are batched and sent during preferred hours. The engine uses Firebase Cloud Messaging for push, Twilio for SMS, and Amazon SES for email. Notification content never includes PHI — it contains only a prompt to log in and view details.

Database Design

The portal's primary database is PostgreSQL with tables for portal-specific data (not duplicating EHR clinical data). Core tables: users (user_id, patient_id_in_ehr, email_encrypted, phone_encrypted, mfa_method, preferred_language, notification_preferences JSONB, created_at, last_login), proxy_grants (grant_id, grantor_patient_id, grantee_user_id, relationship_type, access_scope JSONB, valid_from, valid_to, verification_status, verified_by), messages (message_id, thread_id, sender_id, sender_type PATIENT/PROVIDER, recipient_id, encrypted_body_s3_key, attachments JSONB, sent_at, read_at), consent_directives (consent_id, patient_id, directive_type, policy JSONB, status ACTIVE/REVOKED, effective_date, authored_date).

The billing tables: invoices (invoice_id, patient_id, encounter_id, amount_due, amount_paid, status, due_date, line_items JSONB), payments (payment_id, invoice_id, patient_id, amount, payment_method, transaction_id, status, processed_at). Redis caches: session store with 30-minute idle timeout, FHIR response cache keyed by patient_id+resource_type with 15-minute TTL.

API Design

  • GET /v1/health-records/summary — Returns aggregated patient health summary including recent encounters, active medications, allergies, and flagged abnormal results; uses FHIR Patient/$everything under the hood
  • GET /v1/health-records/labs?from=2024-01-01&to=2024-12-31&category=chemistry — Retrieve lab results with patient-friendly interpretations; supports filtering by date range and category
  • POST /v1/messages — Send a secure message to care team; body contains thread_id (optional for new thread), recipient_provider_id, subject, encrypted_body, attachments
  • POST /v1/prescriptions/{rx_id}/refill — Request a prescription refill; body contains pharmacy_ncpdp_id, preferred_pickup_date; routes through provider approval workflow

Scaling & Bottlenecks

The lab result release spike is the primary scaling challenge. When a health system batch-releases lab results at 5 PM, 50,000 patients receive notifications and many log in simultaneously. The BFF layer handles this with autoscaling (AWS ECS with target-tracking on CPU, scaling from 10 to 50 instances within 3 minutes). The Redis FHIR cache is critical during spikes — without it, 50,000 patients each triggering 5+ FHIR API calls would overwhelm the upstream EHR with 250,000 requests in minutes. The cache absorbs repeat requests (e.g., patient refreshes the page) and the 15-minute TTL ensures data freshness. The upstream EHR FHIR server is rate-limited to 500 requests/sec from the portal, enforced by a token bucket in the Health Records Service.

Secure messaging at 200K messages/day is modest, but attachment storage (clinical photos, documents) can grow significantly. Attachments are stored in S3 with presigned URLs (15-minute expiry) for secure download without proxying through the application layer. The Billing Service integrates with external payment processors (Stripe for card payments, Plaid for bank transfers), and payment processing latency (1-3 seconds) is handled with optimistic UI updates and webhook-based confirmation.

Key Trade-offs

  • FHIR proxy over local data store for clinical records: Acting as a FHIR client to the EHR avoids data duplication and ensures the portal always shows the latest clinical data, but introduces dependency on EHR availability and adds network latency — mitigated by Redis caching and graceful degradation (showing cached data with a staleness warning)
  • 15-minute cache TTL over real-time FHIR subscriptions: A short cache TTL balances data freshness with EHR load, but means newly posted results may take up to 15 minutes to appear — FHIR Subscription push notifications for high-priority events (abnormal labs) trigger immediate cache invalidation
  • Client-side encryption for messages over server-side only: Client-side encryption with per-thread keys ensures the portal operator cannot read message content, strengthening HIPAA posture, but prevents server-side search across message content — mitigated by client-side search on locally decrypted messages
  • OPA-based policy enforcement over application-level access checks: Centralizing authorization in OPA provides consistent, auditable access control across all services, but adds 5-10ms latency per request for policy evaluation — OPA decisions are cached for 60 seconds per (user, resource, action) tuple

GO DEEPER

Master this topic in our 12-week cohort

Our Advanced System Design cohort covers this and 11 other deep-dive topics with live sessions, assignments, and expert feedback.