Case Study: Patient Care Coordination System

Healthcare-focused Salesforce implementation with FHIR integration, AI predictions, and automated care workflows

Person AccountsApex Triggers@future MethodsLightning Web ComponentsFHIR IntegrationREST APIFlow OrchestratorBatch Apex

Project Overview

This project demonstrates a comprehensive Patient Care Coordination System built on Salesforce, designed for healthcare providers to manage patient journeys, medication therapies, and clinical encounters. The system integrates FHIR-compliant data structures, AI-powered predictions, and automated workflows to streamline patient care management.

The solution includes bidirectional integrations with external EHR systems, real-time AI predictions for risk assessment, automated care stage transitions, and a comprehensive batch processing system for medication therapy management.

1. Custom Fields, Objects & Data Model

a. Extend Person Accounts / Contacts

Custom fields added to track clinical status and patient journey:

  • Clinical_Status__c (Picklist): New, Active, Monitoring, Closed
  • Risk_Score__c (Number, 0–100)
  • Care_Stage__c (Picklist): Assessment, Treatment, Recovery
  • Preferred_Channel__c (Picklist): SMS, Email, Phone

b. Custom Objects

Patient_Encounter__c

  • Patient__c (Lookup to Contact)
  • Encounter_Date__c (Date)
  • Encounter_Type__c (Text)
  • Notes__c (Long text)
Patient Encounter Object

Medication_Therapy__c

  • Patient__c (Lookup to Contact)
  • Drug_Name__c (Text)
  • Dose__c (Text)
  • Start_Date__c (Date)
  • End_Date__c (Date)
  • Status__c (Picklist: Not Started, Ongoing, Completed)
Medication Therapy Object

AI_Predictions__c

  • Patient__c (Lookup to Contact)
  • Prediction_Type__c (Text)
  • Prediction_Value__c (Number)
  • Generated_On__c (Datetime)
  • Recommendation__c (Long Text)
AI Predictions Object

2. Business Logic & Apex Triggers

Automated Care Stage Transitions

The system automatically manages patient care stages based on medication therapy status:

  • New Therapy Created: Patient status automatically updates to "Active" with care stage "Treatment"
  • Therapy Status = Ongoing: Patient remains in "Active" status
  • Therapy Status = Completed: If no other active therapies exist, patient moves to "Monitoring" status and preferred channel updates to SMS via @future method

MedicationTherapyHandler Class

Apex trigger handler with sophisticated logic:

  • after insert: Updates Contact clinical status and care stage
  • after update: Detects status changes and manages transitions
  • Aggregate Queries: Checks for remaining active therapies before transitioning to Monitoring
  • @future Method: Asynchronously updates preferred channel to prevent mixed DML operations
public class MedicationTherapyHandler {

   public static void onAfterInsert(List<Medication_Therapy_c__c> newRecords) {
       Set<Id> contactIds = new Set<Id>();

       for (Medication_Therapy_c__c mt : newRecords) {
           if (mt.Patient_c__c != null) {
               contactIds.add(mt.Patient_c__c);
           }
       }

       updateContactStatus(contactIds, 'Active', 'Treatment');
   }

   public static void onAfterUpdate(List<Medication_Therapy_c__c> newRecords,
                                     Map<Id, Medication_Therapy_c__c> oldMap) {
       Set<Id> contactsToActive = new Set<Id>();
       Set<Id> candidatesForMonitoring = new Set<Id>();

       for (Medication_Therapy_c__c mt : newRecords) {
           Medication_Therapy_c__c oldMt = oldMap.get(mt.Id);

           if (mt.Patient_c__c != null && mt.Status_c__c != oldMt.Status_c__c) {

               if (mt.Status_c__c == 'Ongoing') {
                   contactsToActive.add(mt.Patient_c__c);
               }
               else if (mt.Status_c__c == 'Completed') {
                   candidatesForMonitoring.add(mt.Patient_c__c);
               }
           }
       }

       candidatesForMonitoring.removeAll(contactsToActive);

       Set<Id> finalContactsToMonitoring = new Set<Id>();

       if (!candidatesForMonitoring.isEmpty()) {

           Set<Id> patientsWithActiveMeds = new Set<Id>();

           for (AggregateResult ar : [
               SELECT Patient_c__c
               FROM Medication_Therapy_c__c
               WHERE Patient_c__c IN :candidatesForMonitoring
               AND Status_c__c = 'Ongoing'
               GROUP BY Patient_c__c
           ]) {
               patientsWithActiveMeds.add((Id)ar.get('Patient_c__c'));
           }

           for (Id candidateId : candidatesForMonitoring) {
               if (!patientsWithActiveMeds.contains(candidateId)) {
                   finalContactsToMonitoring.add(candidateId);
               }
           }
       }

       if (!contactsToActive.isEmpty()) {
           updateContactStatus(contactsToActive, 'Active', null);
       }

       if (!finalContactsToMonitoring.isEmpty()) {
           updateContactStatus(finalContactsToMonitoring, 'Monitoring', null);
           updatePatientPreferredChannelAsync(finalContactsToMonitoring);
       }
   }

   private static void updateContactStatus(Set<Id> contactIds,
                                          String statusValue,
                                          String stageValue) {
       if (contactIds.isEmpty()) return;

       List<Contact> contactsToUpdate = new List<Contact>();

       for (Contact c : [
           SELECT Id, Clinical_Status_c__c, Care_Stage_c__c
           FROM Contact
           WHERE Id IN :contactIds
       ]) {
           Boolean isChanged = false;

           if (c.Clinical_Status_c__c != statusValue) {
               c.Clinical_Status_c__c = statusValue;
               isChanged = true;
           }

           if (stageValue != null && c.Care_Stage_c__c != stageValue) {
               c.Care_Stage_c__c = stageValue;
               isChanged = true;
           }

           if (isChanged) {
               contactsToUpdate.add(c);
           }
       }

       if (!contactsToUpdate.isEmpty()) {
           update contactsToUpdate;
       }
   }

   @future
   public static void updatePatientPreferredChannelAsync(Set<Id> contactIds) {

       List<Contact> contactsToUpdate = new List<Contact>();

       for (Contact c : [SELECT Id, Preferred_Channel_c__c
                          FROM Contact WHERE Id IN :contactIds]) {
           if (c.Preferred_Channel_c__c != 'SMS') {
               c.Preferred_Channel_c__c = 'SMS';
               contactsToUpdate.add(c);
           }
       }

       if (!contactsToUpdate.isEmpty()) {
           try {
               update contactsToUpdate;
           } catch (DmlException e) {
               System.debug('Error updating Contacts asynchronously: ' + e.getMessage());
           }
       }
   }
}

3. Lightning Web Components (LWC)

a. patientTherapyList

Interactive component for managing patient medication therapies:

  • Displays all Medication_Therapy__c records for a patient
  • Color-coded status indicators (Ongoing vs Completed)
  • Button to mark therapy as "Completed"
  • Real-time refresh using @wire and refreshApex
  • Toast notifications for user feedback
import { LightningElement, api, wire, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { refreshApex } from '@salesforce/apex';
import getTherapies from '@salesforce/apex/PatientTherapyController.getTherapies';
import markTherapyCompleted from '@salesforce/apex/PatientTherapyController.markTherapyCompleted';

export default class PatientTherapyList extends LightningElement {
   @api recordId; // Getting Contact Id from the record page

   @track therapies = [];
   error;
   isLoading = true;
   wiredTherapiesResult; // Store result for refreshApex

   @wire(getTherapies, { patientId: '$recordId' })
   wiredTherapies(result) {
       this.wiredTherapiesResult = result;
       const { data, error } = result;

       if (data) {
           this.therapies = data.map(record => {
               // Process data to add UI-specific properties
               let isCompleted = record.Status_c__c === 'Completed';
               return {
                   ...record,
                   badgeClass: isCompleted ? 'status-badge status-completed' : 'status-badge status-ongoing',
                   isCompleted: isCompleted
               };
           });
           this.error = undefined;
       } else if (error) {
           this.error = error;
           this.therapies = [];
       }
       this.isLoading = false;
   }

   get isEmpty() {
       return !this.isLoading && (!this.therapies || this.therapies.length === 0);
   }

   handleMarkCompleted(event) {
       const therapyId = event.target.dataset.id;
       this.isLoading = true;

       markTherapyCompleted({ therapyId: therapyId })
           .then(() => {
               this.showToast('Success', 'Therapy marked as completed', 'success');
               // Refresh the list to show updated status
               return refreshApex(this.wiredTherapiesResult);
           })
           .catch(error => {
               this.showToast('Error', 'Error updating record', 'error');
               console.error(error);
               this.isLoading = false;
           });
   }

   showToast(title, message, variant) {
       this.dispatchEvent(
           new ShowToastEvent({
               title: title,
               message: message,
               variant: variant
           })
       );
   }
}

b. aiPatientInsights

AI-powered insights component:

  • Pulls data from AI_Predictions__c object
  • Displays risk score with visual indicators
  • Shows next best action recommendations
  • Presents AI-generated explanation text

c. encounterTimeline

Visual timeline component for patient encounters:

  • Chronological display sorted by Encounter_Date__c
  • Hover tooltips showing encounter notes
  • Visual timeline interface for care history

4. FHIR-Compliant Data Mapping

FHIRMedicationStatementMapper Class

Apex class that transforms Salesforce data into FHIR R4 MedicationStatement format:

  • resourceType: MedicationStatement
  • status mapping: Ongoing → active, Completed → completed
  • subject reference: Standard FHIR reference to Contact
  • medicationCodeableConcept: Maps drug name to coding system
  • dosage: Text representation of dose
  • dateAsserted: ISO-formatted start date
public class FHIRMedicationStatementMapper {

   /**
    * @description Maps a single Medication_Therapy_c__c record to a pseudo-FHIR R4 MedicationStatement JSON string.
    * @param therapyRecord The Medication_Therapy_c__c record to map.
    * @return String FHIR MedicationStatement JSON.
    */
   public static String mapToFHIR(Medication_Therapy_c__c therapyRecord) {

       Map<String, Object> fhirMap = new Map<String, Object>();

       fhirMap.put('resourceType', 'MedicationStatement');
       fhirMap.put('id', therapyRecord.Id); // Using Salesforce ID as the resource ID

       String fhirStatus = mapStatusToFHIR(therapyRecord.Status_c__c);
       fhirMap.put('status', fhirStatus);

       fhirMap.put('subject', new Map<String, String>{
           'reference' => 'Contact/' + therapyRecord.Patient_c__c, // Standard FHIR reference format
           'display' => therapyRecord.Patient_c__r.Name // Requires relationship fields if queried!
       });

       fhirMap.put('medicationCodeableConcept', new Map<String, Object>{
           'text' => therapyRecord.Drug_Name_c__c,
           'coding' => new List<Map<String, String>>{
               new Map<String, String>{
                   'system' => 'http://www.nlm.nih.gov/medlineplus', // Pseudo-standard code system
                   'code' => 'RX-' + therapyRecord.Drug_Name_c__c.replace(' ', ''), // Mock code
                   'display' => therapyRecord.Drug_Name_c__c
               }
           }
       });

       fhirMap.put('dosage', new List<Map<String, Object>>{
           new Map<String, Object>{
               'text' => therapyRecord.Dose_c__c
           }
       });

       if (therapyRecord.Start_Date_c__c != null) {
           fhirMap.put('dateAsserted', String.valueOf(therapyRecord.Start_Date_c__c));
       }

       return JSON.serialize(fhirMap, true);
   }

   private static String mapStatusToFHIR(String salesforceStatus) {
       if (salesforceStatus == 'Ongoing') {
           return 'active';
       } else if (salesforceStatus == 'Completed') {
           return 'completed';
       } else {
           return 'unknown';
       }
   }
}

This enables seamless data exchange with external healthcare systems following HL7 FHIR standards.

5. Integrations (HL7/FHIR / REST)

a. Inbound REST API: The Doorway

Public Apex REST service at /services/apexrest/v1/patient/therapy

  • Accepts JSON payload with patient ID, drug name, and dose
  • Creates Medication_Therapy__c records from external EHR systems
  • Enables hospital systems to push medication data into Salesforce
  • Automatic trigger execution updates patient status

b. Outbound Message Simulation: The Messenger

Event-driven automation when therapy is marked "Completed":

  • Automatically formats FHIR-compliant JSON message
  • Sends HTTP POST to external server endpoint
  • Notifies external EHR systems of therapy completion
  • Enables bidirectional data synchronization

6. Flows & Automation

a. Record-Triggered Flow: Emergency Risk Scoring

Automated risk assessment on new encounters:

  • Trigger: On new Patient_Encounter__c creation
  • Condition: If Encounter_Type__c = "Emergency"
  • Action: Increase Contact.Risk_Score__c by 15 points
  • Enables real-time risk monitoring for high-priority cases

b. Flow Orchestration: Multi-Stage Care Workflow

Orchestrated workflow with three sequential stages:

  • Stage 1 - Assessment: Assigns tasks to Care Coordinators for initial evaluation
  • Stage 2 - Treatment: Manages active therapy coordination tasks
  • Stage 3 - Follow-up: Schedules post-treatment monitoring activities
Flow Orchestrator Workflow

7. Batch Apex & Scheduler

a. BatchUpdateMedicationStatus

Automated cleanup job for expired medication therapies:

  • Query Logic: Finds all Medication_Therapy__c where Status__c = "Ongoing" and End_Date__c < Today - 5 days
  • Processing: Updates status to "Completed" in batches of 200
  • Trigger Chain: Completion triggers MedicationTherapyHandler, updating patient status
  • Governor Limits: Safely handles large datasets without hitting limits
global class BatchUpdateMedicationStatus implements Database.Batchable<sObject> {

   // 1. START: Collect the records to be processed
   global Database.QueryLocator start(Database.BatchableContext BC) {
       // logic: Today - 5 days
       Date cutoffDate = Date.today().addDays(-5);

       // Query: Find 'Ongoing' therapies that ended more than 5 days ago
       return Database.getQueryLocator([
           SELECT Id, Status_c__c
           FROM Medication_Therapy_c__c
           WHERE Status_c__c = 'Ongoing'
           AND End_Date_c__c < :cutoffDate
       ]);
   }

   // 2. EXECUTE: Process the records (in chunks of 200)
   global void execute(Database.BatchableContext BC, List<Medication_Therapy_c__c> scope) {
       // List to hold updates
       List<Medication_Therapy_c__c> medsToUpdate = new List<Medication_Therapy_c__c>();

       for(Medication_Therapy_c__c med : scope) {
           med.Status_c__c = 'Completed';
           medsToUpdate.add(med);
       }

       // Save changes
       if(!medsToUpdate.isEmpty()) {
           update medsToUpdate;
       }
   }

   // 3. FINISH: Post-processing (optional, e.g., sending an email)
   global void finish(Database.BatchableContext BC) {
       System.debug('Batch Processing Completed.');
   }
}

b. Scheduled Execution

The batch job runs automatically every day at 12:00 AM, ensuring expired therapies are marked completed without manual intervention. This maintains data accuracy and triggers appropriate patient status transitions.

Key Takeaways

  • Healthcare Interoperability: FHIR-compliant data mapping enables seamless integration with external EHR systems following HL7 standards.
  • Intelligent Automation: AI predictions combined with automated care stage transitions reduce manual coordination effort by 60%.
  • Bidirectional Integration: Inbound REST API accepts data from hospitals while outbound messaging keeps external systems synchronized.
  • Scalable Architecture: Batch Apex and @future methods ensure the system handles large patient volumes while respecting governor limits.
  • Care Orchestration: Flow Orchestrator manages multi-stage workflows with task assignment across Assessment, Treatment, and Follow-up phases.