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

Introduction

Welcome back to my series on FHIR Programming using Java and HAPI FHIR Server. In this article, we will cover how to delete FHIR resources using Java. Deleting resources is an essential operation when managing healthcare data, as it allows for the removal of outdated, incorrect, or unnecessary information from your system.

This tutorial builds upon our previous discussions on updating FHIR resources. If you're unfamiliar with reading or updating resources, I recommend reviewing those articles before proceeding. By the end of this article, you'll be equipped to delete FHIR resources securely and efficiently, ensuring your application's data integrity.

Understanding FHIR Delete Semantics

Before implementing delete operations, it's important to understand how FHIR handles deletions and the implications for data integrity.

Hard Delete vs Soft Delete

FHIR servers can implement deletions in different ways:

Soft Delete (Logical Delete) - The most common approach:

  • The resource is marked as deleted but not physically removed from storage
  • Subsequent read requests return a 410 Gone response
  • The resource's history is preserved and can still be accessed via _history
  • The deletion can potentially be undone (server-dependent)
  • Supports audit trails and regulatory compliance requirements

Hard Delete (Physical Delete) - Complete removal:

  • The resource and all its versions are permanently removed from storage
  • No history is retained - the resource is completely gone
  • Cannot be undone
  • May be required for GDPR "right to erasure" or similar privacy regulations
  • Often requires special permissions or configuration

HAPI FHIR Server supports both modes. The expunge operation can be used for hard deletes when necessary:

// Soft delete (standard)
client.delete()
    .resourceById("Patient", "123")
    .execute();

// Hard delete/expunge (if server supports it)
Parameters expungeParams = new Parameters();
expungeParams.addParameter("expungeDeletedResources", new BooleanType(true));
expungeParams.addParameter("expungePreviousVersions", new BooleanType(true));

client.operation()
    .onInstance(new IdType("Patient", "123"))
    .named("$expunge")
    .withParameters(expungeParams)
    .execute();

Referential Integrity

One of the most important considerations when deleting FHIR resources is referential integrity - what happens to other resources that reference the deleted resource?

Common Scenarios:

  • Patient with Observations - If you delete a Patient, what happens to their Observations that reference that Patient?
  • Practitioner with Encounters - Deleting a Practitioner who is referenced in multiple Encounters
  • Organization hierarchy - Deleting an Organization that is the parent of other Organizations

Server Behaviors:

  • Prevent Deletion - Server refuses to delete resources that are referenced by others (returns 409 Conflict)
  • Allow Broken References - Server allows deletion, leaving "dangling" references that return 404 when followed
  • Cascade Delete - Server deletes the resource and all resources that reference it (rarely implemented due to safety concerns)

Most production servers prevent deletion of referenced resources by default:

try {
    client.delete()
        .resourceById("Patient", "123")
        .execute();
} catch (PreconditionFailedException e) {
    // Server refused deletion due to existing references
    System.out.println("Cannot delete: Resource is referenced by other resources");
    System.out.println("Details: " + e.getMessage());
}

Cascade Delete Operations

Some servers support cascade deletion through special parameters or operations:

// Cascade delete (if supported by server)
// This deletes the patient AND all resources that reference it
client.delete()
    .resourceById("Patient", "123")
    .cascade(DeleteCascadeModeEnum.DELETE)
    .execute();

Warning: Cascade deletes can be dangerous and may remove more data than intended. Always verify what will be deleted before executing, and consider whether soft deletion is more appropriate.

History Retention After Deletion

With soft deletes, the resource's history is preserved:

// After deletion, reading the resource returns 410 Gone
try {
    client.read()
        .resource(Patient.class)
        .withId("123")
        .execute();
} catch (ResourceGoneException e) {
    System.out.println("Resource has been deleted (410 Gone)");
}

// But history is still accessible
Bundle history = client.history()
    .onInstance(new IdType("Patient", "123"))
    .returnBundle(Bundle.class)
    .execute();

for (Bundle.BundleEntryComponent entry : history.getEntry()) {
    Patient historicalPatient = (Patient) entry.getResource();
    System.out.println("Version: " + historicalPatient.getMeta().getVersionId());
    System.out.println("Last Updated: " + historicalPatient.getMeta().getLastUpdated());
}

Conditional Delete Considerations

Conditional deletes based on search criteria require careful consideration:

  • No matches - Returns success (nothing to delete is not an error)
  • One match - Deletes that resource
  • Multiple matches - Behavior varies by server:
    • Some servers delete all matching resources
    • Others return an error if more than one resource matches (safer)
    • Check your server's documentation for its specific behavior

Regulatory and Compliance Considerations

Healthcare data deletion is subject to regulatory requirements:

  • HIPAA - Requires retention of medical records for specified periods; deletion must be documented
  • GDPR - Requires "right to erasure" capability; must balance against medical record retention laws
  • Audit Requirements - Many jurisdictions require audit trails that persist even after data deletion

Always consult with compliance experts before implementing delete functionality in production healthcare systems.

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 art of life lies in a constant readjustment to our surroundings.” ~ Kakuzo Okakura

Step 1 of 3: Import Required Classes

To delete FHIR resources, we need to import specific classes from the HAPI FHIR library. These imports will enable us to interact with the FHIR server and perform the deletion operation. 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.client.api.IGenericClient;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.IdType;

These imports provide access to the FHIR context and client classes required for communication with the FHIR server, as well as the `MethodOutcome` class to capture the result of the delete operation.

Step 2 of 3: Deleting a FHIR Resource

Once the necessary imports are in place, we can proceed to delete a FHIR resource by its ID. The following example demonstrates how to delete a `Patient` resource:

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

        // Create a client to interact with the FHIR server
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

        try {
            // Verify the resource exists before attempting to delete
            Patient patientToDelete = null;
            try {
                patientToDelete = client.read()
                    .resource(Patient.class)
                    .withId("123")
                    .execute();
                
                System.out.println("Found patient with ID: " + patientToDelete.getId());
                
                // Optional: Display patient details before deletion
                String patientName = patientToDelete.hasName() ? 
                    patientToDelete.getNameFirstRep().getNameAsSingleString() : 
                    "Unknown";
                System.out.println("Patient to delete: " + patientName);
                
            } catch (Exception e) {
                System.err.println("Patient with ID 123 does not exist or cannot be retrieved: " + e.getMessage());
                return;
            }

            // Delete the Patient resource by its ID
            MethodOutcome outcome = client.delete()
                .resourceById("Patient", "123")
                .execute();

            // Print the outcome of the delete operation
            System.out.println("Deletion status: " + (outcome.getOperationOutcome() != null ? 
                "Successfully processed with operation outcome" : 
                "Successfully processed without operation outcome"));
            
            // Verify deletion by attempting to retrieve the resource again
            try {
                Patient deletedPatient = client.read()
                    .resource(Patient.class)
                    .withId("123")
                    .execute();
                
                System.out.println("Warning: Resource still exists after deletion attempt!");
            } catch (Exception e) {
                System.out.println("Verification successful: Resource has been deleted");
            }
            
        } catch (Exception e) {
            System.err.println("Error during deletion process: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

In this snippet, the `Patient` resource with the ID "123" is deleted from the FHIR server. The `MethodOutcome` object captures the result of the operation, allowing you to confirm whether the deletion was successful. Additionally, the code includes verification steps both before and after the deletion to ensure the operation is performed correctly.

Step 3 of 3: Handling Conditional Deletes

In certain cases, you may want to delete resources conditionally, based on specific criteria rather than a unique ID. The following example shows how to perform a conditional delete operation on a `Patient` resource:

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

        // Create a client to interact with the FHIR server
        IGenericClient client = ctx.newRestfulGenericClient("http://localhost:8080/fhir");

        try {
            // Before deletion, check how many resources match the criteria
            int matchCount = client.search()
                .forResource(Patient.class)
                .where(Patient.IDENTIFIER.exactly().systemAndCode("http://hospital.org/mrns", "12345"))
                .count()
                .execute();
                
            System.out.println("Found " + matchCount + " patient(s) matching the criteria");
            
            if (matchCount == 0) {
                System.out.println("No matching resources to delete. Exiting.");
                return;
            } else if (matchCount > 1) {
                System.out.println("Warning: Multiple resources match the criteria. Deletion may affect multiple records.");
                // Depending on your application's requirements, you might want to cancel the operation here
            }

            // Perform a conditional delete operation
            MethodOutcome outcome = client.delete()
                .resourceConditionalByUrl("Patient?identifier=http://hospital.org/mrns|12345")
                .execute();

            System.out.println("Conditional deletion status: " + (outcome.getOperationOutcome() != null ? 
                "Successfully processed with operation outcome" : 
                "Successfully processed without operation outcome"));
                
            // Verify deletion by checking if any resources still match the criteria
            int postDeleteCount = client.search()
                .forResource(Patient.class)
                .where(Patient.IDENTIFIER.exactly().systemAndCode("http://hospital.org/mrns", "12345"))
                .count()
                .execute();
                
            if (postDeleteCount == 0) {
                System.out.println("Verification successful: All matching resources have been deleted");
            } else {
                System.out.println("Warning: " + postDeleteCount + " matching resource(s) still exist after deletion attempt");
            }
            
        } catch (Exception e) {
            System.err.println("Error during conditional deletion process: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

In this example, any `Patient` resource with the identifier system "http://hospital.org/mrns" and value "12345" will be deleted. Conditional deletes are useful in scenarios where resources need to be removed based on certain attributes or conditions rather than specific IDs. The code also includes pre-deletion counting and post-deletion verification to ensure the operation behaves as expected.

Conclusion

In this article, we've explored the process of deleting FHIR resources using Java and the HAPI FHIR library. We covered how to delete a specific resource by its ID and how to perform conditional deletions based on criteria. These operations are crucial for maintaining the accuracy and relevance of data within your healthcare applications.

With the knowledge gained from this tutorial, you can now effectively manage the lifecycle of FHIR resources in your application. For further exploration of FHIR programming, stay tuned for the next article in this series, where we will dive into searching FHIR resources in depth.