FHIR Programming using Java and HAPI FHIR Server - Updating FHIR Resources

Introduction

Welcome back to my series on FHIR Programming using Java and HAPI FHIR Server. In this tutorial, we will explore how to update FHIR resources on a HAPI FHIR Server using Java. Building on the concepts from our previous discussions on reading FHIR resources, this article will guide you through the steps to modify existing healthcare data in a FHIR-compliant format.

Updating FHIR resources is a common operation in healthcare applications where data accuracy and currency are crucial. Whether you're updating patient information, medication records, or other resources, understanding how to efficiently perform these updates is essential. This article will cover the process of updating FHIR resources, ensuring that you can maintain and manage your healthcare data effectively.

Understanding FHIR Update Semantics

Before diving into the code, it's important to understand the different ways FHIR handles resource modifications and the mechanisms in place to prevent data conflicts.

Full Replacement (PUT) vs Partial Update (PATCH)

FHIR supports two distinct approaches to updating resources:

Full Replacement (PUT) - The standard FHIR update operation:

  • Replaces the entire resource with the new version you provide
  • You must send the complete resource, not just the changed fields
  • Any elements not included in your update will be removed from the resource
  • Best practice: Always read the current version first, modify it, then update

Partial Update (PATCH) - Allows updating specific fields without sending the entire resource:

  • Uses either JSON Patch (RFC 6902) or FHIRPath Patch format
  • Only the fields you specify are modified; other fields remain unchanged
  • More efficient for small changes to large resources
  • Reduces the risk of accidentally overwriting concurrent changes
// JSON Patch example - add a phone number
String jsonPatch = "[{\"op\": \"add\", \"path\": \"/telecom/0\", \"value\": {\"system\": \"phone\", \"value\": \"555-1234\"}}]";

// Execute patch operation
MethodOutcome outcome = client.patch()
    .withId("Patient/123")
    .withBody(jsonPatch)
    .execute();

ETags and Optimistic Locking

FHIR uses HTTP ETags (Entity Tags) for optimistic concurrency control, preventing the "lost update" problem where two users might overwrite each other's changes:

  • ETag Header - When you read a resource, the server returns an ETag (usually the versionId) in the response header
  • If-Match Header - When updating, you can send the ETag in the If-Match header to ensure you're updating the version you think you're updating
  • 409 Conflict - If someone else modified the resource since you read it, the server returns a conflict error
// Read the patient and capture the version
Patient patient = client.read()
    .resource(Patient.class)
    .withId("123")
    .execute();

String versionId = patient.getMeta().getVersionId();

// Modify the patient...
patient.getNameFirstRep().setFamily("NewName");

// Update with version-aware check (If-Match)
MethodOutcome outcome = client.update()
    .resource(patient)
    .withId(patient.getIdElement())
    .execute();

// If successful, the server accepted our update
// If 409 Conflict, someone else modified it first

This optimistic locking approach allows multiple users to read and work with resources simultaneously while preventing silent data loss from concurrent updates.

Conditional Update Operations

Conditional updates let you update a resource based on search criteria rather than a known ID. This is useful when:

  • You know business identifiers but not the server's logical ID
  • You want to create-or-update in a single operation (upsert)
  • You're synchronizing data from external systems

The conditional update uses search parameters in the URL:

  • No matches - Creates a new resource (if the server supports it)
  • One match - Updates that resource
  • Multiple matches - Returns an error (412 Precondition Failed)

Common conditional update patterns:

// Update by business identifier
client.update()
    .resource(patient)
    .conditionalByUrl("Patient?identifier=http://hospital.org/mrn|12345")
    .execute();

// Update by multiple criteria
client.update()
    .resource(patient)
    .conditionalByUrl("Patient?identifier=http://hospital.org/mrn|12345&birthdate=1980-01-01")
    .execute();

Version-Aware Updates and Conflict Resolution

When conflicts occur, your application needs a strategy to handle them:

  1. Read-Modify-Write with Retry - Re-read the current version, apply your changes, and try again
  2. Merge Changes - Compare the conflicting versions and intelligently merge the differences
  3. User Resolution - Present both versions to the user and let them decide
  4. Last Writer Wins - Simply overwrite (use cautiously, may lose data)
// Example: Read-Modify-Write with retry pattern
int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++) {
    try {
        Patient patient = client.read()
            .resource(Patient.class)
            .withId("123")
            .execute();

        // Apply your modifications
        patient.setActive(true);

        // Attempt update
        client.update()
            .resource(patient)
            .execute();

        System.out.println("Update successful");
        break;

    } catch (PreconditionFailedException e) {
        System.out.println("Conflict detected, retrying... (attempt " + (attempt + 1) + ")");
        if (attempt == maxRetries - 1) {
            throw new RuntimeException("Failed to update after " + maxRetries + " attempts");
        }
    }
}

Server-Side Update Behavior

Different FHIR servers may handle updates differently. Important considerations:

  • Version Increment - Most servers increment versionId on every update, even if content didn't change
  • lastUpdated - The server typically sets this timestamp; client-provided values are usually ignored
  • Validation - Servers may validate the updated resource against profiles before accepting
  • Business Rules - Servers may enforce additional constraints (e.g., preventing certain status transitions)

Prerequisites

Ensure that the following prerequisites are met before starting:

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

“The good life is one inspired by love and guided by knowledge.” ~ Bertrand Russell

Step 1 of 3: Import Required Classes

To begin updating FHIR resources, we need to import specific classes from the HAPI FHIR library. These imports will allow us to interact with the FHIR server and modify the resources as needed. Open your `App.java` file and include the following imports:

package com.saravanansubramanian.fhir;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.hl7.fhir.r4.model.*;

import java.util.UUID;

These imports include the FHIR context and client classes required for communication with the server, as well as all the R4 model classes for managing FHIR resources, and `MethodOutcome` to capture the result of the update operation.

Step 2 of 3: Update a FHIR Resource

With the necessary imports in place, we can now update an existing FHIR resource. In the example below, we will modify a `Patient` resource by updating the patient's name, adding a contact method, and changing their gender. The following code snippet demonstrates how to perform this update:

public class FhirUpdateResources {
    public static void main(String[] args) {
        // Replace with your FHIR server base URL
        String fhirServerUrl = "http://hapi.fhir.org/baseR4";

        // Initialize FHIR context and client
        FhirContext ctx = FhirContext.forR4();
        IGenericClient client = ctx.newRestfulGenericClient(fhirServerUrl);

        try {
            // Read the existing patient resource by ID
            Patient patient = client.read()
                .resource(Patient.class)
                .withId("123")
                .execute();
            
            System.out.println("Retrieved existing patient with ID: " + patient.getId());
            
            // Update the patient's name
            if (patient.hasName()) {
                patient.getNameFirstRep().setFamily("Smith");
                System.out.println("Updated family name to: Smith");
            } else {
                HumanName name = new HumanName();
                name.setFamily("Smith");
                name.addGiven("John");
                patient.addName(name);
                System.out.println("Added new name: John Smith");
            }
            
            // Add a phone contact if none exists
            boolean hasPhone = false;
            for (ContactPoint contact : patient.getTelecom()) {
                if (contact.getSystem() == ContactPointSystem.PHONE) {
                    hasPhone = true;
                    break;
                }
            }
            
            if (!hasPhone) {
                ContactPoint phoneContact = new ContactPoint();
                phoneContact.setSystem(ContactPointSystem.PHONE);
                phoneContact.setValue("555-123-4567");
                phoneContact.setUse(ContactPointUse.HOME);
                patient.addTelecom(phoneContact);
                System.out.println("Added phone contact: 555-123-4567");
            }
            
            // Update gender
            patient.setGender(AdministrativeGender.MALE);
            System.out.println("Set gender to: MALE");
            
            // Update birth date using java.time (thread-safe)
            LocalDate birthDate = LocalDate.parse("1980-01-15", DateTimeFormatter.ISO_LOCAL_DATE);
            patient.setBirthDate(Date.from(birthDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
            System.out.println("Updated birth date to: 1980-01-15");

            // Perform the update on the server
            MethodOutcome outcome = client.update()
                .resource(patient)
                .execute();

            // Print the outcome of the update operation with null-safe access
            System.out.println("Update status: " + (outcome.getCreated() ? "Created" : "Updated"));
            String resourceId = (outcome.getId() != null) ? outcome.getId().getValue() : "N/A";
            System.out.println("Updated resource ID: " + resourceId);
            
            // Retrieve the updated patient to verify changes
            Patient updatedPatient = client.read()
                .resource(Patient.class)
                .withId(outcome.getId().getIdPart())
                .execute();
                
            String encoded = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(updatedPatient);
            System.out.println("Updated Patient Resource:");
            System.out.println(encoded);
            
        } catch (Exception e) {
            System.err.println("Error updating patient: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

This snippet reads an existing patient resource with the ID "123" from the FHIR server, updates various attributes, and then sends the updated resource back to the server. The `MethodOutcome` object captures the result of the update operation, indicating whether the update was successful. Finally, we retrieve the updated patient to verify the changes.

Step 3 of 3: Conditional Updates

In scenarios where you want to update a resource conditionally—based on certain criteria—you can use a conditional update. This feature is particularly useful when the resource to be updated is not identified by a specific ID. The following code demonstrates how to perform a conditional update:

// Conditional Update Example
// This can be part of your FhirUpdateResources class

// Initialize FHIR context and client
String fhirServerUrl = "http://hapi.fhir.org/baseR4";
FhirContext ctx = FhirContext.forR4();
IGenericClient client = ctx.newRestfulGenericClient(fhirServerUrl);

        try {
            // Create a patient resource to update
            Patient patient = new Patient();
            
            // Set patient identifiers (for conditional update matching)
            patient.addIdentifier()
                .setSystem("http://hospital.org/mrns")
                .setValue("12345");
                
            // Set patient name
            patient.addName()
                .setFamily("Johnson")
                .addGiven("Robert");
                
            // Set patient gender
            patient.setGender(AdministrativeGender.MALE);
            
            // Add phone contact
            ContactPoint phoneContact = new ContactPoint();
            phoneContact.setSystem(ContactPointSystem.PHONE);
            phoneContact.setValue("555-987-6543");
            phoneContact.setUse(ContactPointUse.HOME);
            patient.addTelecom(phoneContact);
            
            // Perform a conditional update based on the patient's identifier
            MethodOutcome outcome = client.update()
                .resource(patient)
                .conditionalByUrl("Patient?identifier=http://hospital.org/mrns|12345")
                .execute();

            // Print the outcome of the conditional update operation with null-safe access
            System.out.println("Conditional update status: " + (outcome.getCreated() ? "Created" : "Updated"));

            if (outcome.getId() != null) {
                System.out.println("Resource ID: " + outcome.getId().getValue());

                // Retrieve the updated or created patient
                Patient resultPatient = client.read()
                    .resource(Patient.class)
                    .withId(outcome.getId().getIdPart())
                    .execute();

                // Display patient name with null-safe checks
                if (resultPatient.hasName()) {
                    HumanName name = resultPatient.getNameFirstRep();
                    String givenNames = name.hasGiven()
                        ? String.join(" ", name.getGiven().stream().map(StringType::getValue).collect(java.util.stream.Collectors.toList()))
                        : "";
                    System.out.println("Patient Name: " + name.getFamily() + ", " + givenNames);
                }

                String encoded = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resultPatient);
                System.out.println("Result Patient Resource:");
                System.out.println(encoded);
            }
            
        } catch (Exception e) {
            System.err.println("Error performing conditional update: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

In this example, the update is applied to any patient resource with an identifier system of "http://hospital.org/mrns" and value of "12345." If such a resource exists, it will be updated; otherwise, a new resource may be created depending on the server's configuration. The conditional update is a powerful tool for ensuring data consistency in complex systems.

Conclusion

In this article, we covered the process of updating FHIR resources using Java and the HAPI FHIR library. We discussed how to modify an existing resource, perform updates, and utilize conditional updates based on specific criteria. These skills are vital for managing and maintaining healthcare data within your applications.

In the next tutorial in this series, we will explore how to delete FHIR resources, completing the CRUD operations necessary for full data management in FHIR-compliant systems.