šŸ“š Radiology Information Systems Advanced 19 min read

How to Build a Modern RIS with Seamless PACS and EHR Integration

Complete guide to developing a production-ready Radiology Information System with DICOM connectivity, HL7 interfacing, and cloud-based image distribution.

āœļø
Dr. David Martinez, MD, CIIP

A modern Radiology Information System (RIS) must orchestrate complex workflows spanning scheduling, image acquisition, radiologist assignment, interpretation, and result delivery—all while integrating seamlessly with PACS for images, EHR for orders and results, and billing systems for charges. This guide shows you how to build a production-ready RIS that handles 100,000+ exams annually with 99.9% uptime and sub-second response times.

Understanding RIS Architecture

A comprehensive RIS manages the entire radiology workflow:

Pre-Examination:

  • Order entry and management
  • Exam scheduling and resource allocation
  • Patient registration and insurance verification
  • Exam preparation instructions
  • Protocol selection

Examination:

  • Modality worklist management (DICOM)
  • Image acquisition coordination
  • Quality control and verification
  • Technologist documentation

Interpretation:

  • Radiologist assignment and worklist
  • Image viewing integration (PACS)
  • Report creation (voice recognition, templates)
  • Critical finding notification
  • Peer consultation

Post-Examination:

  • Report finalization and distribution
  • Image and report archiving
  • Billing and coding
  • Quality metrics and analytics

JustCopy.ai’s RIS template includes all components pre-configured, with 10 AI agents handling database design, DICOM integration, HL7 messaging, report generation, and EHR connectivity automatically.

System Architecture

// Modern RIS architecture
interface RISArchitecture {
  // Frontend applications
  applications: {
    scheduling: SchedulingWorkstation;
    technologist: TechWorkstation;
    radiologist: ReadingWorkstation;
    referring: ReferringPhysicianPortal;
    patient: PatientPortal;
    admin: AdministrativeConsole;
  };

  // Core RIS services
  core Services: {
    orderManagement: OrderManagementService;
    scheduling: SchedulingEngine;
    worklist: WorklistManagement;
    reporting: ReportingEngine;
    distribution: ResultDistribution;
  };

  // Integration layer
  integrations: {
    pacs: PACSInterface;  // DICOM connectivity
    ehr: EHRInterface;    // HL7 messaging
    modalities: ModalityWorklistServer;  // DICOM MWL
    billing: BillingInterface;
    voiceRecognition: SpeechRecognitionEngine;
  };

  // Data layer
  dataServices: {
    examDatabase: ExamRepository;
    reportDatabase: ReportRepository;
    imageMetadata: ImageIndexService;
    analytics: AnalyticsEngine;
  };

  // AI capabilities
  aiFeatures: {
    scheduling: AIScheduler;
    protocolSelection: ProtocolAI;
    criticalFindingDetection: CriticalFindingAI;
    reportQA: ReportQualityAI;
    predictiveAnalytics: PredictiveModels;
  };
}

JustCopy.ai provides this complete architecture out-of-the-box, with AI agents configuring each component for your specific needs.

Database Schema

-- RIS database schema
CREATE SCHEMA radiology;

-- Exam orders
CREATE TABLE radiology.exam_orders (
  order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  accession_number VARCHAR(50) UNIQUE NOT NULL,  -- Unique exam identifier

  -- Patient information
  patient_id UUID NOT NULL REFERENCES patients(patient_id),
  patient_mrn VARCHAR(50) NOT NULL,

  -- Ordering information
  ordered_by_provider_id UUID REFERENCES providers(provider_id),
  ordering_provider_npi VARCHAR(20),
  ordered_at TIMESTAMP NOT NULL DEFAULT NOW(),

  -- Exam details
  modality VARCHAR(10) NOT NULL,  -- CT, MRI, XR, US, NM, PET, MG
  exam_code VARCHAR(20) NOT NULL,  -- CPT code
  exam_description TEXT NOT NULL,
  body_part VARCHAR(100),
  laterality VARCHAR(10),  -- left, right, bilateral

  -- Clinical information
  clinical_indication TEXT,
  clinical_history TEXT,
  relevant_prior_exams TEXT[],
  icd10_codes TEXT[],

  -- Exam requirements
  with_contrast BOOLEAN DEFAULT FALSE,
  contrast_type VARCHAR(50),
  protocol_name VARCHAR(255),
  special_instructions TEXT,

  -- Scheduling
  priority VARCHAR(20),  -- stat, urgent, routine
  requested_date DATE,
  scheduled_datetime TIMESTAMP,
  scheduled_scanner VARCHAR(100),
  estimated_duration_minutes INTEGER,

  -- Status tracking
  order_status VARCHAR(50) NOT NULL,  -- pending, scheduled, in-progress, completed, cancelled
  status_updated_at TIMESTAMP DEFAULT NOW(),

  -- Exam performance
  exam_start_time TIMESTAMP,
  exam_end_time TIMESTAMP,
  performing_technologist VARCHAR(255),

  -- Reading
  assigned_radiologist_id UUID REFERENCES providers(provider_id),
  reading_started_at TIMESTAMP,
  preliminary_report_at TIMESTAMP,
  final_report_at TIMESTAMP,
  reported_by_radiologist_id UUID REFERENCES providers(provider_id),

  -- Imaging
  image_count INTEGER DEFAULT 0,
  pacs_study_uid VARCHAR(255),  -- DICOM Study Instance UID

  -- Billing
  billing_code VARCHAR(20),
  billing_status VARCHAR(50),
  billed_at TIMESTAMP,

  -- Metadata
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Reports
CREATE TABLE radiology.reports (
  report_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  accession_number VARCHAR(50) NOT NULL REFERENCES radiology.exam_orders(accession_number),

  -- Report content
  report_text TEXT NOT NULL,
  impression TEXT,
  findings TEXT,
  technique TEXT,
  comparison TEXT,

  -- Structured reporting
  structured_findings JSONB,  -- Machine-readable findings

  -- Report metadata
  report_status VARCHAR(50) NOT NULL,  -- draft, preliminary, final, amended
  report_version INTEGER DEFAULT 1,

  -- Authoring
  dictated_by_radiologist_id UUID REFERENCES providers(provider_id),
  dictated_at TIMESTAMP,
  transcribed_by VARCHAR(255),
  transcribed_at TIMESTAMP,
  signed_by_radiologist_id UUID REFERENCES providers(provider_id),
  signed_at TIMESTAMP,

  -- Amendments
  amended_from_report_id UUID REFERENCES radiology.reports(report_id),
  amendment_reason TEXT,

  -- Critical findings
  critical_finding BOOLEAN DEFAULT FALSE,
  critical_finding_notified_at TIMESTAMP,
  critical_finding_acknowledged_by VARCHAR(255),

  -- Distribution
  distributed_to_ehr BOOLEAN DEFAULT FALSE,
  distributed_at TIMESTAMP,

  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Report templates
CREATE TABLE radiology.report_templates (
  template_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  template_name VARCHAR(255) NOT NULL,
  modality VARCHAR(10),
  body_part VARCHAR(100),

  -- Template content
  template_text TEXT NOT NULL,
  sections JSONB,  -- Structured template sections

  -- Usage
  created_by_radiologist_id UUID REFERENCES providers(provider_id),
  times_used INTEGER DEFAULT 0,
  is_active BOOLEAN DEFAULT TRUE,

  created_at TIMESTAMP DEFAULT NOW()
);

-- Radiologist worklist
CREATE TABLE radiology.radiologist_worklist (
  worklist_item_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  radiologist_id UUID NOT NULL REFERENCES providers(provider_id),
  accession_number VARCHAR(50) NOT NULL REFERENCES radiology.exam_orders(accession_number),

  -- Priority and ordering
  priority_score NUMERIC(5,2),  -- AI-calculated priority
  assigned_at TIMESTAMP DEFAULT NOW(),
  due_by TIMESTAMP,

  -- Status
  status VARCHAR(50),  -- pending, in-progress, completed
  started_at TIMESTAMP,
  completed_at TIMESTAMP,

  -- Workflow
  is_urgent BOOLEAN DEFAULT FALSE,
  requires_consultation BOOLEAN DEFAULT FALSE,
  consultation_with_radiologist_id UUID REFERENCES providers(provider_id)
);

-- Critical findings tracking
CREATE TABLE radiology.critical_findings (
  critical_finding_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  accession_number VARCHAR(50) NOT NULL,
  report_id UUID REFERENCES radiology.reports(report_id),

  -- Finding details
  finding_category VARCHAR(100),  -- pulmonary-embolism, pneumothorax, etc.
  finding_description TEXT NOT NULL,
  finding_severity VARCHAR(20),

  -- Notification
  identified_at TIMESTAMP NOT NULL DEFAULT NOW(),
  identified_by_radiologist_id UUID REFERENCES providers(provider_id),

  notified_to_provider_id UUID REFERENCES providers(provider_id),
  notification_method VARCHAR(50),  -- phone, page, secure-message
  notified_at TIMESTAMP,
  acknowledged_at TIMESTAMP,
  acknowledged_by VARCHAR(255),

  -- Follow-up
  action_taken TEXT,
  escalated BOOLEAN DEFAULT FALSE,
  escalated_to VARCHAR(255),
  escalated_at TIMESTAMP,

  -- Compliance
  notification_compliant BOOLEAN,  -- Met time requirement
  required_notification_time_minutes INTEGER,
  actual_notification_time_minutes INTEGER
);

-- Image metadata (links to PACS)
CREATE TABLE radiology.image_metadata (
  image_metadata_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  accession_number VARCHAR(50) NOT NULL,

  -- DICOM identifiers
  study_instance_uid VARCHAR(255) UNIQUE NOT NULL,
  series_instance_uid VARCHAR(255) NOT NULL,
  sop_instance_uid VARCHAR(255),

  -- Image details
  modality VARCHAR(10) NOT NULL,
  series_number INTEGER,
  instance_number INTEGER,
  image_count INTEGER,

  -- Acquisition info
  acquisition_datetime TIMESTAMP,
  scanner_model VARCHAR(255),
  scanner_serial_number VARCHAR(100),

  -- Storage
  pacs_location VARCHAR(500),  -- URL or path in PACS
  archived BOOLEAN DEFAULT FALSE,
  archive_location VARCHAR(500),

  created_at TIMESTAMP DEFAULT NOW()
);

-- Quality metrics
CREATE TABLE radiology.quality_metrics (
  metric_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  accession_number VARCHAR(50) NOT NULL,

  -- TAT metrics
  order_to_schedule_minutes INTEGER,
  schedule_to_exam_minutes INTEGER,
  exam_to_prelim_minutes INTEGER,
  prelim_to_final_minutes INTEGER,
  order_to_final_minutes INTEGER,

  -- Quality indicators
  critical_finding_notification_minutes INTEGER,
  repeat_exam BOOLEAN DEFAULT FALSE,
  repeat_reason TEXT,

  -- Patient experience
  patient_wait_time_minutes INTEGER,
  patient_satisfaction_score INTEGER,

  recorded_at TIMESTAMP DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_orders_patient ON radiology.exam_orders(patient_id);
CREATE INDEX idx_orders_status ON radiology.exam_orders(order_status);
CREATE INDEX idx_orders_scheduled ON radiology.exam_orders(scheduled_datetime);
CREATE INDEX idx_orders_modality ON radiology.exam_orders(modality);
CREATE INDEX idx_reports_accession ON radiology.reports(accession_number);
CREATE INDEX idx_worklist_radiologist ON radiology.radiologist_worklist(radiologist_id, status);
CREATE INDEX idx_images_study_uid ON radiology.image_metadata(study_instance_uid);

JustCopy.ai’s AI agents generate optimized database schemas based on your volume, with proper indexing, partitioning, and performance tuning.

DICOM Integration

// DICOM connectivity for modality worklist and image storage
class DICOMInterface {
  private dimseServer: DIMSEServer;
  private storeSCP: StoreSCP;

  async initialize() {
    // Start DICOM services
    await this.startModalityWorklistServer();
    await this.startStorageServer();
  }

  async startModalityWorklistServer() {
    // DICOM Modality Worklist (MWL) server
    // Provides scheduled exams to modalities (CT, MRI, etc.)

    this.dimseServer = new DIMSEServer({
      port: 104,  // Standard DICOM port
      ae_title: 'RIS_MWL',
      max_connections: 50
    });

    // Handle C-FIND requests from modalities
    this.dimseServer.on('C-FIND-RQ', async (request, response) => {
      // Modality is requesting worklist
      const query = request.dataset;

      // Get scheduled exams for this modality
      const scheduled_exams = await db.exam_orders.find({
        scheduled_datetime: {
          $gte: query.ScheduledProcedureStepStartDate,
          $lte: addDays(query.ScheduledProcedureStepStartDate, 1)
        },
        scheduled_scanner: query.ScheduledStationAETitle,
        order_status: 'scheduled'
      });

      // Return each exam as DICOM dataset
      for (const exam of scheduled_exams) {
        const dataset = this.buildMWLDataset(exam);
        response.send('pending', dataset);
      }

      response.send('success');  // All results sent
    });

    await this.dimseServer.listen();
  }

  buildMWLDataset(exam: ExamOrder): DICOMDataset {
    return {
      // Scheduled Procedure Step Sequence
      '00400100': [{
        // Scheduled Procedure Step Start Date
        '00400002': format(exam.scheduled_datetime, 'YYYYMMDD'),

        // Scheduled Procedure Step Start Time
        '00400003': format(exam.scheduled_datetime, 'HHmmss'),

        // Modality
        '00080060': exam.modality,

        // Scheduled Station AE Title
        '00400001': exam.scheduled_scanner,

        // Scheduled Procedure Step Description
        '00400007': exam.exam_description,

        // Scheduled Protocol Code Sequence
        '00400008': [{
          '00080100': exam.protocol_code,
          '00080102': 'RIS',
          '00080104': exam.protocol_name
        }],

        // Scheduled Procedure Step ID
        '00400009': exam.accession_number
      }],

      // Patient information
      '00100010': `${exam.patient.last_name}^${exam.patient.first_name}`,  // Patient Name
      '00100020': exam.patient_mrn,  // Patient ID
      '00100030': format(exam.patient.dob, 'YYYYMMDD'),  // Patient Birth Date
      '00100040': exam.patient.gender,  // Patient Sex

      // Study information
      '0020000D': generateStudyUID(exam.accession_number),  // Study Instance UID
      '00080050': exam.accession_number,  // Accession Number

      // Requesting Physician
      '00321032': `${exam.ordering_provider.last_name}^${exam.ordering_provider.first_name}`,

      // Requested Procedure Description
      '00321060': exam.exam_description
    };
  }

  async startStorageServer() {
    // DICOM Storage SCP - receives images from modalities
    this.storeSCP = new StoreSCP({
      port: 11112,  // Secondary port for storage
      ae_title: 'RIS_STORE',
      storage_path: config.tempImageStorage
    });

    // Handle C-STORE requests (image upload)
    this.storeSCP.on('C-STORE-RQ', async (request) => {
      const dataset = request.dataset;

      // Extract DICOM metadata
      const metadata = {
        study_instance_uid: dataset['0020000D'],
        series_instance_uid: dataset['0020000E'],
        sop_instance_uid: dataset['00080018'],
        accession_number: dataset['00080050'],
        modality: dataset['00080060'],
        acquisition_datetime: this.parseDICOMDateTime(
          dataset['00080021'],  // Series Date
          dataset['00080031']   // Series Time
        )
      };

      // Store image metadata in RIS database
      await db.image_metadata.create(metadata);

      // Forward image to PACS for permanent storage
      await this.forwardToPACS(dataset, metadata);

      // Update exam status
      await db.exam_orders.update({
        accession_number: metadata.accession_number,
        pacs_study_uid: metadata.study_instance_uid,
        image_count: { $inc: 1 },  // Increment image count
        order_status: 'in-progress'
      });

      return { status: 'success' };
    });

    await this.storeSCP.listen();
  }

  async forwardToPACS(dataset: DICOMDataset, metadata: ImageMetadata) {
    // Send image to PACS for archiving
    const pacsConnection = new DICOMConnection({
      host: config.pacs.host,
      port: config.pacs.port,
      ae_title: config.pacs.ae_title,
      calling_ae_title: 'RIS'
    });

    await pacsConnection.store(dataset);
    await pacsConnection.close();

    // Log forwarding
    await db.image_metadata.update({
      sop_instance_uid: metadata.sop_instance_uid,
      pacs_location: `dicom://${config.pacs.host}:${config.pacs.port}/${metadata.study_instance_uid}`,
      archived: true
    });
  }
}

JustCopy.ai handles all DICOM connectivity automatically, supporting all modalities and PACS systems with pre-configured interfaces.

Reporting Engine

// Radiology reporting with voice recognition
class ReportingEngine {
  private voiceRecognition: SpeechRecognitionEngine;
  private templateEngine: TemplateEngine;

  async createReport(accession_number: string, radiologist: Radiologist) {
    // Get exam details
    const exam = await db.exam_orders.findOne({ accession_number });

    // Load appropriate template
    const template = await this.selectTemplate(exam);

    // Create draft report
    const report = await db.reports.create({
      accession_number: accession_number,
      report_text: template.template_text,
      report_status: 'draft',
      dictated_by_radiologist_id: radiologist.id
    });

    return report;
  }

  async dictateFinding(report_id: string, audio: AudioStream) {
    // Voice recognition transcription
    const transcript = await this.voiceRecognition.transcribe(audio);

    // Medical terminology correction
    const corrected = await this.correctMedicalTerms(transcript);

    // Update report
    await db.reports.update({
      report_id: report_id,
      findings: { $concat: [{ $fields: 'findings' }, '\n\n', corrected] },
      dictated_at: new Date()
    });

    return corrected;
  }

  async detectCriticalFinding(report: Report) {
    // AI analyzes report text for critical findings
    const analysis = await criticalFindingAI.analyze({
      report_text: report.report_text,
      impression: report.impression,
      modality: report.exam.modality,
      body_part: report.exam.body_part
    });

    if (analysis.critical_findings.length > 0) {
      // Create critical finding record
      for (const finding of analysis.critical_findings) {
        const critical = await db.critical_findings.create({
          accession_number: report.accession_number,
          report_id: report.report_id,
          finding_category: finding.category,
          finding_description: finding.description,
          finding_severity: finding.severity,
          identified_by_radiologist_id: report.signed_by_radiologist_id
        });

        // Immediately notify ordering provider
        await this.notifyCriticalFinding(critical, report.exam);
      }

      // Mark report
      await db.reports.update({
        report_id: report.report_id,
        critical_finding: true
      });
    }

    return analysis;
  }

  async notifyCriticalFinding(finding: CriticalFinding, exam: ExamOrder) {
    // Multi-channel notification
    const provider = exam.ordering_provider;

    // Try phone first
    const phoneNotification = await phoneSystem.call({
      to: provider.phone,
      message: `CRITICAL FINDING: ${finding.finding_category} for patient ${exam.patient.name}, accession ${exam.accession_number}. Please call radiology immediately.`,
      priority: 'urgent',
      repeat_if_no_answer: 3,
      escalate_after_minutes: 15
    });

    if (phoneNotification.answered) {
      await db.critical_findings.update({
        critical_finding_id: finding.critical_finding_id,
        notification_method: 'phone',
        notified_at: new Date(),
        notified_to_provider_id: provider.id
      });

      // Wait for acknowledgment
      await this.awaitAcknowledgment(finding, provider);
    } else {
      // Escalate to backup contact
      await this.escalateCriticalFinding(finding, exam);
    }
  }
}

JustCopy.ai’s reporting engine includes voice recognition, template management, critical finding detection, and automated notification—all working together seamlessly.

Best Practices

  1. DICOM Compliance: Test with all modality vendors before go-live
  2. Accession Number Format: Use meaningful, sequential numbers
  3. Critical Finding SOP: Define clear policies and test notification workflows
  4. Report Templates: Start with 10-15 high-volume exam templates
  5. Radiologist Training: 4-hour hands-on training minimum

JustCopy.ai provides implementation support with AI agents handling system configuration, DICOM testing, and staff training.

Conclusion

A modern RIS with seamless PACS and EHR integration streamlines radiology workflows, improves report turnaround times, and ensures critical findings reach ordering providers immediately. Proper DICOM connectivity eliminates manual data entry, automated worklist management improves scanner productivity, and intelligent reporting tools accelerate radiologist workflows.

JustCopy.ai makes RIS development effortless, with 10 AI agents handling database design, DICOM integration, HL7 messaging, report generation, and critical finding detection automatically.

Ready to build your modern RIS? Explore JustCopy.ai’s radiology solutions and discover how AI-powered development can deliver a production-ready system in weeks.

Streamline radiology. Accelerate care. Start with JustCopy.ai today.

šŸš€

Build This with JustCopy.ai

Skip months of development with 10 specialized AI agents. JustCopy.ai can copy, customize, and deploy this application instantly. Our AI agents write code, run tests, handle deployment, and monitor your application—all following healthcare industry best practices and HIPAA compliance standards.