FHIR Programming using Java and HAPI FHIR Server - Chaining FHIR Operations

Introduction

Welcome back to my series on FHIR Programming using Java and HAPI FHIR Server. In this article, we will delve into the powerful concept of chaining FHIR operations. Chaining allows you to perform complex queries and operations by linking multiple resources and actions together in a single request, optimizing both performance and code readability.

This tutorial builds on our previous discussion on validating FHIR resources. If you're unfamiliar with the basics of FHIR resource validation, I recommend reviewing that article before proceeding. By the end of this article, you'll be able to efficiently chain FHIR operations, enhancing the efficiency of your healthcare application workflows.

Understanding FHIR Chaining and References

Before diving into code, let's understand the theoretical foundations of FHIR references and how chaining leverages them for powerful queries.

Resource Reference Mechanics

FHIR resources link to each other through References. Understanding the types of references is crucial for effective chaining:

Literal References - Point to a resource by its URL:

  • Absolute URL: http://server.com/fhir/Patient/123 - Full URL including server
  • Relative URL: Patient/123 - Resource type and ID only (most common)
  • Version-specific: Patient/123/_history/2 - Points to a specific version

Logical References - Point to a resource by business identifier (not a URL):

  • Uses identifier element instead of reference
  • Example: "identifier": {"system": "http://hospital.org/mrn", "value": "12345"}
  • Useful when the actual resource URL isn't known or stable
  • Requires server resolution to find the actual resource

Contained References - Point to a resource embedded within the parent:

  • Uses # prefix: #local-id
  • The referenced resource is included in the parent's contained element
  • Used when the referenced resource has no independent existence

Forward Chaining

Forward chaining allows you to search for resources based on properties of resources they reference. The syntax uses a dot (.) to "chain" through the reference:

  • Pattern: [search-parameter]:[resource-type].[chained-parameter]=[value]
  • Example: Observation?subject:Patient.name=Smith
  • Meaning: Find Observations where the subject (a Patient) has name matching "Smith"

Chains can go multiple levels deep:

  • Observation?subject:Patient.organization:Organization.name=General Hospital
  • Finds Observations for Patients whose managing Organization has a specific name

Reverse Chaining (_has)

Reverse chaining (using _has) works in the opposite direction - finding resources that are referenced BY other resources:

  • Pattern: [ResourceType]?_has:[RefResource]:[ref-param]:[search-param]=[value]
  • Example: Patient?_has:Observation:subject:code=8867-4
  • Meaning: Find Patients who are the subject of an Observation with code 8867-4

This is powerful for questions like:

  • "Which patients have lab results above a certain threshold?"
  • "Which practitioners have prescribed a specific medication?"
  • "Which organizations have patients with a certain condition?"

Performance Implications of Chained Searches

While chaining is powerful, it has performance implications:

  • Database Joins - Each chain level typically requires an additional database join, which can be expensive
  • No Indexing - Some servers may not have indexes optimized for chained parameters
  • Timeout Risks - Complex multi-level chains can timeout on large datasets

Best Practices for Performance:

  • Limit chain depth when possible (1-2 levels is usually optimal)
  • Add additional non-chained criteria to narrow results early
  • Use _count to limit result sets
  • Consider multiple simple queries instead of one complex chain
  • Check your server's CapabilityStatement to see which chains are supported
// Good: Combine chaining with other criteria to narrow results
Bundle results = client.search()
    .forResource(Observation.class)
    .where(Observation.CODE.exactly().code("8867-4"))  // Narrow by code first
    .and(Observation.SUBJECT.hasChainedProperty(
        Patient.FAMILY.matches().value("Smith")))
    .count(50)  // Limit results
    .returnBundle(Bundle.class)
    .execute();

_include and _revinclude: Alternatives to Chaining

Instead of chaining, you can use _include and _revinclude to retrieve related resources in a single query:

  • _include - Include resources that matched resources reference
  • _revinclude - Include resources that reference the matched resources
// Get Observations AND their referenced Patients in one query
Bundle results = client.search()
    .forResource(Observation.class)
    .where(Observation.CODE.exactly().code("8867-4"))
    .include(Observation.INCLUDE_SUBJECT)  // Include the Patient
    .returnBundle(Bundle.class)
    .execute();

// Results contain both Observations and Patients

This approach can be more efficient than chaining when you need to retrieve the related resources anyway.

GraphQL: An Alternative for Complex Queries

For very complex queries, FHIR supports GraphQL as an alternative to chaining:

  • Allows precise specification of exactly which fields to return
  • Can traverse multiple resource relationships in a single query
  • More efficient when you only need specific fields (reduces payload size)
  • Better suited for complex, hierarchical data retrieval

Example GraphQL query structure:

{
  PatientList(name: "Smith") {
    id
    name { family given }
    Observations: ObservationList(_reference: subject) {
      code { coding { code display } }
      valueQuantity { value unit }
    }
  }
}

GraphQL support varies by server. Check your server's CapabilityStatement for $graphql operation support.

Prerequisites

Before you start, ensure your environment is ready:

  • Java Development Kit (JDK) is installed and configured.
  • Apache Maven is installed and set up for your project.
  • HAPI FHIR Server is running and accessible.
  • You can find all the code demonstrated in this tutorial on GitHub here

“The limits of my language mean the limits of my world.” ~ Ludwig Wittgenstein

Step 1 of 4: Understanding FHIR Operation Chaining

FHIR operation chaining allows you to perform a series of linked operations in a single request. This is particularly useful when dealing with related resources, such as a `Patient` and their associated `Observation` records. Chaining operations can significantly reduce the number of API calls needed, improving both performance and scalability.

Let's start by setting up our environment and importing the necessary classes:

package com.saravanansubramanian.fhir;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
import ca.uhn.fhir.rest.gclient.StringClientParam;
import ca.uhn.fhir.rest.gclient.TokenClientParam;
import ca.uhn.fhir.rest.param.DateRangeParam;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.instance.model.api.IBaseResource;

import java.util.Date;
import java.util.List;
import java.util.ArrayList;

Now, let's implement a basic example of chaining operations to search for resources:

public class BasicChainingExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a FHIR client
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

        try {
            // Example 1: Find all Observations linked to a specific Patient
            Bundle results = client.search()
                .forResource(Observation.class)
                .where(Observation.SUBJECT.hasId("Patient/123"))
                .returnBundle(Bundle.class)
                .execute();

            System.out.println("Example 1: Observations for Patient/123");
            System.out.println("Found " + results.getTotal() + " observations");
            
            // Process and display results with safe type checking
            for (Bundle.BundleEntryComponent entry : results.getEntry()) {
                Resource resource = entry.getResource();

                // Safe type check using instanceof with pattern matching
                if (resource instanceof Observation obs) {
                    System.out.println("\nObservation ID: " + obs.getIdElement().getIdPart());

                    if (obs.hasCode() && obs.getCode().hasText()) {
                        System.out.println("Code: " + obs.getCode().getText());
                    } else if (obs.hasCode() && obs.getCode().hasCoding()) {
                        Coding coding = obs.getCode().getCodingFirstRep();
                        System.out.println("Code: " +
                            (coding.hasDisplay() ? coding.getDisplay() : coding.getCode()));
                    }

                    if (obs.hasEffectiveDateTimeType()) {
                        System.out.println("Effective Date: " + obs.getEffectiveDateTimeType().getValue());
                    }

                    if (obs.hasValueQuantity()) {
                        Quantity quantity = obs.getValueQuantity();
                        System.out.println("Value: " + quantity.getValue() + " " + quantity.getUnit());
                    }
                }
            }
            
            // Example 2: Find all Appointments for a specific Patient
            Bundle appointmentResults = client.search()
                .forResource(Appointment.class)
                .where(Appointment.PATIENT.hasId("Patient/123"))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nExample 2: Appointments for Patient/123");
            System.out.println("Found " + appointmentResults.getTotal() + " appointments");
            
            for (Bundle.BundleEntryComponent entry : appointmentResults.getEntry()) {
                Resource resource = entry.getResource();

                // Safe type check using instanceof with pattern matching
                if (resource instanceof Appointment appointment) {
                    System.out.println("\nAppointment ID: " + appointment.getIdElement().getIdPart());

                    if (appointment.hasStart()) {
                        System.out.println("Start Time: " + appointment.getStart());
                    }

                    if (appointment.hasStatus()) {
                        System.out.println("Status: " + appointment.getStatus().getDisplay());
                    }

                    if (appointment.hasDescription()) {
                        System.out.println("Description: " + appointment.getDescription());
                    }
                }
            }
            
        } catch (Exception e) {
            System.err.println("Error performing basic chaining: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

In this example, we chain operations to find all `Observation` resources linked to a specific `Patient`, and then all `Appointment` resources for the same patient. This demonstrates the basic concept of chaining operations by referencing related resources.

Step 2 of 4: Advanced Chaining Techniques

Beyond basic chaining, FHIR supports more complex chaining scenarios, such as filtering results or chaining multiple resources. The following example demonstrates these advanced techniques:

public class AdvancedChainingExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a FHIR client
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

        try {
            // Example 1: Find Observations for a Patient with a specific medical record number
            Bundle results1 = client.search()
                .forResource(Observation.class)
                .where(Observation.SUBJECT.hasChainedProperty(
                    Patient.IDENTIFIER.exactly().systemAndCode(
                        "http://hospital.org/mrns", "12345")))
                .returnBundle(Bundle.class)
                .execute();

            System.out.println("Example 1: Observations for Patient with MRN 12345");
            System.out.println("Found " + results1.getTotal() + " observations");
            printObservationResults(results1);
            
            // Example 2: Find Observations with a specific code for a Patient with a specific family name
            Bundle results2 = client.search()
                .forResource(Observation.class)
                .where(Observation.CODE.exactly().code("8480-6")) // Systolic Blood Pressure
                .and(Observation.SUBJECT.hasChainedProperty(
                    Patient.FAMILY.matches().value("Smith")))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nExample 2: Blood Pressure Observations for Patients named Smith");
            System.out.println("Found " + results2.getTotal() + " observations");
            printObservationResults(results2);
            
            // Example 3: Find all MedicationRequests prescribed by a specific practitioner
            Bundle results3 = client.search()
                .forResource(MedicationRequest.class)
                .where(MedicationRequest.REQUESTER.hasChainedProperty(
                    Practitioner.IDENTIFIER.exactly().systemAndCode(
                        "http://hospital.org/pracs", "PRAC-123")))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("\nExample 3: Medications prescribed by Practitioner PRAC-123");
            System.out.println("Found " + results3.getTotal() + " medication requests");
            
            for (Bundle.BundleEntryComponent entry : results3.getEntry()) {
                Resource resource = entry.getResource();

                // Safe type check using instanceof with pattern matching
                if (resource instanceof MedicationRequest med) {
                    System.out.println("\nMedication ID: " + med.getIdElement().getIdPart());

                    if (med.hasMedicationCodeableConcept()) {
                        CodeableConcept cc = med.getMedicationCodeableConcept();
                        System.out.println("Medication: " + (cc.hasText() ? cc.getText() :
                            (cc.hasCoding() ? cc.getCodingFirstRep().getDisplay() : "Unknown")));
                    }

                    if (med.hasAuthoredOn()) {
                        System.out.println("Prescribed Date: " + med.getAuthoredOn());
                    }

                    if (med.hasStatus()) {
                        System.out.println("Status: " + med.getStatus().getDisplay());
                    }

                    if (med.hasSubject()) {
                        System.out.println("Subject: " + med.getSubject().getReference());
                    }
                }
            }
            
            // Example 4: Find Patients who have a specific allergy using reverse chaining (_has)
            // Note: Patient.LINK is for linking to other Patient resources, not allergies
            // To find patients with allergies, we use _has:AllergyIntolerance:patient:code
            Bundle results4 = client.search()
                .forResource(Patient.class)
                .where(new StringClientParam("_has:AllergyIntolerance:patient:code")
                    .matches().value("371924009")) // Penicillin allergy (SNOMED CT)
                .returnBundle(Bundle.class)
                .execute();

            System.out.println("\nExample 4: Patients with Penicillin allergy (using reverse chaining)");
            System.out.println("Found " + results4.getTotal() + " patients");

            for (Bundle.BundleEntryComponent entry : results4.getEntry()) {
                Resource resource = entry.getResource();

                // Safe type check using instanceof with pattern matching
                if (resource instanceof Patient patient) {
                    System.out.println("\nPatient ID: " + patient.getIdElement().getIdPart());

                    if (patient.hasName() && !patient.getName().isEmpty()) {
                        HumanName name = patient.getNameFirstRep();
                        String givenNames = name.hasGiven()
                            ? String.join(" ", name.getGiven().stream().map(StringType::getValue).collect(java.util.stream.Collectors.toList()))
                            : "";
                        System.out.println("Name: " + name.getFamily() + ", " + givenNames);
                    }

                    if (patient.hasIdentifier()) {
                        Identifier identifier = patient.getIdentifierFirstRep();
                        System.out.println("Identifier: " +
                            (identifier.hasSystem() ? identifier.getSystem() + "|" : "") +
                            identifier.getValue());
                    }
                }
            }
            
        } catch (Exception e) {
            System.err.println("Error performing advanced chaining: " + e.getMessage());
            e.printStackTrace();
        }
    }
    
    private static void printObservationResults(Bundle results) {
        for (Bundle.BundleEntryComponent entry : results.getEntry()) {
            Resource resource = entry.getResource();

            // Safe type check using instanceof with pattern matching
            if (resource instanceof Observation obs) {
                System.out.println("\nObservation ID: " + obs.getIdElement().getIdPart());

                if (obs.hasCode() && obs.getCode().hasText()) {
                    System.out.println("Code: " + obs.getCode().getText());
                } else if (obs.hasCode() && obs.getCode().hasCoding()) {
                    Coding coding = obs.getCode().getCodingFirstRep();
                    System.out.println("Code: " +
                        (coding.hasDisplay() ? coding.getDisplay() : coding.getCode()));
                }

                if (obs.hasValueQuantity()) {
                    Quantity quantity = obs.getValueQuantity();
                    System.out.println("Value: " + quantity.getValue() + " " + quantity.getUnit());
                }

                if (obs.hasSubject()) {
                    System.out.println("Subject: " + obs.getSubject().getReference());
                }
            }
        }
    }
}

This example demonstrates advanced chaining techniques, including chaining through multiple resources, using multiple criteria, and combining chained searches with other search parameters. These techniques allow for highly specific queries that can retrieve exactly the data needed for a particular use case.

Step 3 of 4: Creating and Managing Resource Relationships

To effectively use chaining operations, it's important to understand how to create and manage relationships between FHIR resources. The following example demonstrates how to establish these relationships:

public class ResourceRelationshipExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a FHIR client
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            
            // Step 1: Create a Patient resource
            Patient patient = new Patient();
            patient.addIdentifier()
                .setSystem("http://hospital.org/mrns")
                .setValue("MRN-98765");
            patient.addName()
                .setFamily("Johnson")
                .addGiven("Michael");
            patient.setGender(Enumerations.AdministrativeGender.MALE);
            patient.setBirthDate(sdf.parse("1975-05-15"));
            
            // Save the Patient resource
            System.out.println("Creating Patient resource...");
            MethodOutcome patientOutcome = client.create()
                .resource(patient)
                .execute();
                
            String patientId = patientOutcome.getId().getIdPart();
            System.out.println("Patient created with ID: " + patientId);
            
            // Step 2: Create a Practitioner resource
            Practitioner practitioner = new Practitioner();
            practitioner.addIdentifier()
                .setSystem("http://hospital.org/pracs")
                .setValue("PRAC-45678");
            practitioner.addName()
                .setFamily("Wilson")
                .addGiven("Robert");
                
            // Save the Practitioner resource
            System.out.println("\nCreating Practitioner resource...");
            MethodOutcome practitionerOutcome = client.create()
                .resource(practitioner)
                .execute();
                
            String practitionerId = practitionerOutcome.getId().getIdPart();
            System.out.println("Practitioner created with ID: " + practitionerId);
            
            // Step 3: Create an Observation resource linked to the Patient
            Observation observation = new Observation();
            observation.setStatus(Observation.ObservationStatus.FINAL);
            observation.setCode(
                new CodeableConcept().addCoding(
                    new Coding()
                        .setSystem("http://loinc.org")
                        .setCode("8480-6")
                        .setDisplay("Systolic Blood Pressure")));
            
            // Set subject reference to link to the Patient
            observation.setSubject(new Reference("Patient/" + patientId));
            
            // Set the observer to the Practitioner
            observation.setPerformer(
                List.of(new Reference("Practitioner/" + practitionerId)));
            
            // Set the observation value
            observation.setValue(
                new Quantity()
                    .setValue(120)
                    .setUnit("mm[Hg]")
                    .setSystem("http://unitsofmeasure.org")
                    .setCode("mm[Hg]"));
            
            observation.setEffective(new DateTimeType(new Date()));
            
            // Save the Observation resource
            System.out.println("\nCreating Observation resource linked to Patient and Practitioner...");
            MethodOutcome observationOutcome = client.create()
                .resource(observation)
                .execute();
                
            String observationId = observationOutcome.getId().getIdPart();
            System.out.println("Observation created with ID: " + observationId);
            
            // Step 4: Create a MedicationRequest linked to both Patient and Practitioner
            MedicationRequest medicationRequest = new MedicationRequest();
            medicationRequest.setStatus(MedicationRequest.MedicationRequestStatus.ACTIVE);
            medicationRequest.setIntent(MedicationRequest.MedicationRequestIntent.ORDER);
            
            // Set medication information
            medicationRequest.setMedication(
                new CodeableConcept().addCoding(
                    new Coding()
                        .setSystem("http://www.nlm.nih.gov/research/umls/rxnorm")
                        .setCode("314076")
                        .setDisplay("Lisinopril 10 MG Oral Tablet")));
            
            // Link to Patient
            medicationRequest.setSubject(new Reference("Patient/" + patientId));
            
            // Link to Practitioner as requester
            medicationRequest.setRequester(new Reference("Practitioner/" + practitionerId));
            
            // Set authored date
            medicationRequest.setAuthoredOn(new Date());
            
            // Add dosage instructions
            Dosage dosage = new Dosage();
            dosage.setText("1 tablet once daily");
            medicationRequest.addDosageInstruction(dosage);
            
            // Save the MedicationRequest resource
            System.out.println("\nCreating MedicationRequest resource linked to Patient and Practitioner...");
            MethodOutcome medicationOutcome = client.create()
                .resource(medicationRequest)
                .execute();
                
            String medicationRequestId = medicationOutcome.getId().getIdPart();
            System.out.println("MedicationRequest created with ID: " + medicationRequestId);
            
            // Step 5: Now demonstrate retrieving the linked resources using chaining
            System.out.println("\nRetrieving all Observations for the created Patient...");
            Bundle obsResults = client.search()
                .forResource(Observation.class)
                .where(Observation.SUBJECT.hasId("Patient/" + patientId))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("Found " + obsResults.getTotal() + " Observations");
            
            System.out.println("\nRetrieving all MedicationRequests for the created Patient...");
            Bundle medResults = client.search()
                .forResource(MedicationRequest.class)
                .where(MedicationRequest.SUBJECT.hasId("Patient/" + patientId))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("Found " + medResults.getTotal() + " MedicationRequests");
            
            System.out.println("\nRetrieving all resources (Observations and MedicationRequests) created by the Practitioner...");
            Bundle obsForPractitioner = client.search()
                .forResource(Observation.class)
                .where(Observation.PERFORMER.hasId("Practitioner/" + practitionerId))
                .returnBundle(Bundle.class)
                .execute();
                
            Bundle medsForPractitioner = client.search()
                .forResource(MedicationRequest.class)
                .where(MedicationRequest.REQUESTER.hasId("Practitioner/" + practitionerId))
                .returnBundle(Bundle.class)
                .execute();
                
            System.out.println("Found " + obsForPractitioner.getTotal() + " Observations and " + 
                medsForPractitioner.getTotal() + " MedicationRequests by this Practitioner");
            
        } catch (Exception e) {
            System.err.println("Error managing resource relationships: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

This example demonstrates how to create related FHIR resources and establish proper references between them. It creates a `Patient`, a `Practitioner`, an `Observation` linked to both, and a `MedicationRequest` also linked to both. It then demonstrates how to retrieve these linked resources using chained search operations. This approach is crucial for maintaining proper relationships in a FHIR-based system.

Step 4 of 4: Transaction Bundles for Efficient Operation Chaining

For the ultimate in operation chaining efficiency, FHIR provides transaction bundles. These allow multiple operations to be performed in a single atomic transaction. The following example demonstrates this powerful feature:

public class TransactionBundleExample {
    public static void main(String[] args) {
        // Initialize FHIR context
        FhirContext ctx = FhirContext.forR4();

        // Create a FHIR client
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            
            // Create a transaction bundle to perform multiple operations atomically
            Bundle transactionBundle = new Bundle();
            transactionBundle.setType(Bundle.BundleType.TRANSACTION);
            
            // Step 1: Create a new Patient
            Patient patient = new Patient();
            patient.addIdentifier()
                .setSystem("http://hospital.org/mrns")
                .setValue("MRN-54321");
            patient.addName()
                .setFamily("Thompson")
                .addGiven("Emma");
            patient.setGender(Enumerations.AdministrativeGender.FEMALE);
            patient.setBirthDate(sdf.parse("1988-11-20"));
            
            // Add the Patient creation request to the bundle
            transactionBundle.addEntry()
                .setResource(patient)
                .getRequest()
                    .setMethod(Bundle.HTTPVerb.POST)
                    .setUrl("Patient");
                    
            // Step 2: Create a new Practitioner
            Practitioner practitioner = new Practitioner();
            practitioner.addIdentifier()
                .setSystem("http://hospital.org/pracs")
                .setValue("PRAC-87654");
            practitioner.addName()
                .setFamily("Anderson")
                .addGiven("David");
                
            // Add the Practitioner creation request to the bundle
            transactionBundle.addEntry()
                .setResource(practitioner)
                .getRequest()
                    .setMethod(Bundle.HTTPVerb.POST)
                    .setUrl("Practitioner");
                    
            // Step 3: Create an Encounter
            Encounter encounter = new Encounter();
            encounter.setStatus(Encounter.EncounterStatus.FINISHED);
            encounter.setClass_(new Coding()
                .setSystem("http://terminology.hl7.org/CodeSystem/v3-ActCode")
                .setCode("AMB")
                .setDisplay("ambulatory"));
                
            // Set the Patient as the subject (using a temporary ID)
            String tempPatientId = "urn:uuid:" + java.util.UUID.randomUUID().toString();
            patient.setId(tempPatientId);
            encounter.setSubject(new Reference(tempPatientId));
            
            // Set the Practitioner as a participant (using a temporary ID)
            String tempPractitionerId = "urn:uuid:" + java.util.UUID.randomUUID().toString();
            practitioner.setId(tempPractitionerId);
            
            Encounter.EncounterParticipantComponent participant = new Encounter.EncounterParticipantComponent();
            participant.setIndividual(new Reference(tempPractitionerId));
            participant.addType(new CodeableConcept().addCoding(
                new Coding()
                    .setSystem("http://terminology.hl7.org/CodeSystem/v3-ParticipationType")
                    .setCode("PPRF")
                    .setDisplay("primary performer")));
            encounter.addParticipant(participant);
            
            // Set the period
            Period period = new Period();
            period.setStart(new Date());
            period.setEnd(new Date());
            encounter.setPeriod(period);
            
            // Add the Encounter creation request to the bundle
            transactionBundle.addEntry()
                .setResource(encounter)
                .getRequest()
                    .setMethod(Bundle.HTTPVerb.POST)
                    .setUrl("Encounter");
            
            // Step 4: Create an Observation linked to Patient, Practitioner, and Encounter
            Observation observation = new Observation();
            observation.setStatus(Observation.ObservationStatus.FINAL);
            observation.setCode(
                new CodeableConcept().addCoding(
                    new Coding()
                        .setSystem("http://loinc.org")
                        .setCode("8867-4")
                        .setDisplay("Heart rate")));
            
            // Set subject reference to link to the Patient
            observation.setSubject(new Reference(tempPatientId));
            
            // Set the performer to the Practitioner
            observation.setPerformer(List.of(new Reference(tempPractitionerId)));
            
            // Set the encounter
            String tempEncounterId = "urn:uuid:" + java.util.UUID.randomUUID().toString();
            encounter.setId(tempEncounterId);
            observation.setEncounter(new Reference(tempEncounterId));
            
            // Set the observation value
            observation.setValue(
                new Quantity()
                    .setValue(72)
                    .setUnit("beats/minute")
                    .setSystem("http://unitsofmeasure.org")
                    .setCode("/min"));
            
            observation.setEffective(new DateTimeType(new Date()));
            
            // Add the Observation creation request to the bundle
            transactionBundle.addEntry()
                .setResource(observation)
                .getRequest()
                    .setMethod(Bundle.HTTPVerb.POST)
                    .setUrl("Observation");
                    
            // Execute the transaction
            System.out.println("Executing transaction bundle with " + 
                transactionBundle.getEntry().size() + " entries...");
                
            Bundle responseBundle = client.transaction()
                .withBundle(transactionBundle)
                .execute();
                
            System.out.println("Transaction completed successfully!");
            System.out.println("Response bundle has " + responseBundle.getEntry().size() + " entries");
            
            // Process the transaction response
            List<String> createdResourceIds = new ArrayList<>();
            for (Bundle.BundleEntryComponent entry : responseBundle.getEntry()) {
                if (entry.getResponse() != null) {
                    String resourceType = entry.getResponse().getLocation().split("/")[0];
                    String resourceId = entry.getResponse().getLocation().split("/")[1];
                    System.out.println("Created " + resourceType + " with ID: " + resourceId);
                    createdResourceIds.add(entry.getResponse().getLocation());
                }
            }
            
            // Demonstrate retrieving the created resources
            System.out.println("\nRetrieving created resources...");
            
            for (String resourceLocation : createdResourceIds) {
                String[] parts = resourceLocation.split("/");
                String resourceType = parts[0];
                String resourceId = parts[1];
                
                IBaseResource resource = client.read()
                    .resource(resourceType)
                    .withId(resourceId)
                    .execute();
                    
                System.out.println("Retrieved " + resourceType + "/" + resourceId);
                
                // If it's an Observation, show its subject and encounter
                if (resource instanceof Observation) {
                    Observation obs = (Observation) resource;
                    if (obs.hasSubject()) {
                        System.out.println("  Subject: " + obs.getSubject().getReference());
                    }
                    if (obs.hasEncounter()) {
                        System.out.println("  Encounter: " + obs.getEncounter().getReference());
                    }
                }
                
                // If it's an Encounter, show its subject and participants
                if (resource instanceof Encounter) {
                    Encounter enc = (Encounter) resource;
                    if (enc.hasSubject()) {
                        System.out.println("  Subject: " + enc.getSubject().getReference());
                    }
                    if (enc.hasParticipant()) {
                        System.out.println("  Participants: " + 
                            enc.getParticipant().size());
                        for (Encounter.EncounterParticipantComponent part : enc.getParticipant()) {
                            System.out.println("    - " + part.getIndividual().getReference());
                        }
                    }
                }
            }
            
        } catch (Exception e) {
            System.err.println("Error executing transaction bundle: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

This example demonstrates how to use a transaction bundle to create multiple related resources in a single atomic operation. It creates a `Patient`, a `Practitioner`, an `Encounter`, and an `Observation`, all properly linked together. Using transaction bundles ensures that either all operations succeed or all fail, maintaining data integrity. This is especially important when creating a network of related resources.

Conclusion

In this article, we've explored the concept of chaining FHIR operations using Java and the HAPI FHIR library. We covered how to perform basic and advanced chained operations, create and manage relationships between resources, and use transaction bundles for atomic operations. Chaining FHIR operations is a powerful technique that can significantly enhance the performance and maintainability of your healthcare applications.

With the knowledge gained from this tutorial, you can now chain FHIR operations to streamline complex queries and workflows. Stay tuned for the next article in this series, where we will continue to explore advanced FHIR programming techniques.