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

Introduction

Welcome to the next article in my series on FHIR Programming using Java and HAPI FHIR Server. In this tutorial, we will delve into the process of creating FHIR resources using Java. This builds on the foundation laid in the previous article on setting up your development environment. If you haven't set up your environment yet, please refer to the previous tutorial before continuing.

Creating FHIR resources is a fundamental operation when working with healthcare data. Whether you are developing a patient management system, an electronic health record (EHR) system, or a clinical decision support tool, understanding how to create and manage these resources is crucial. In this article, I will guide you through the steps to create a FHIR resource in Java using the HAPI FHIR library, ensuring that you can efficiently handle and store healthcare data in a structured and interoperable format.

Understanding FHIR Resource Structure

Before diving into code, it's essential to understand the hierarchical structure of FHIR resources and how they're organized.

Resource Hierarchy

All FHIR resources inherit from a common hierarchy:

  • Base Resource - The root of all resources, containing the id, meta, implicitRules, and language elements. Meta includes version information, last updated timestamp, and profile declarations.
  • DomainResource - Extends Base and adds text (human-readable narrative), contained (nested resources), extension, and modifierExtension. Most clinical resources inherit from DomainResource.
  • Specific Resources - Concrete types like Patient, Observation, and Condition that add their specific elements.

Cardinality: Understanding Required vs Optional Fields

FHIR uses cardinality notation to specify how many times an element can appear:

  • 0..1 - Optional, at most one (e.g., Patient.birthDate)
  • 0..* - Optional, unlimited (e.g., Patient.name - a patient can have multiple names)
  • 1..1 - Required, exactly one (e.g., Observation.status)
  • 1..* - Required, at least one (e.g., Bundle.entry in a non-empty bundle)

Understanding cardinality helps you know which fields must be populated for a valid resource. Required fields (starting with 1) will cause validation errors if missing.

FHIR Data Types

FHIR defines two categories of data types:

Primitive Types - Simple values with a single element:

  • string - Unicode text
  • boolean - true or false
  • integer - Whole numbers
  • decimal - Rational numbers
  • date - Date only (YYYY, YYYY-MM, or YYYY-MM-DD)
  • dateTime - Date and time with timezone
  • instant - Precise timestamp (used for system times)
  • uri - Uniform Resource Identifier
  • code - Constrained string from a value set

Complex Types - Structured types with multiple child elements:

  • HumanName - Structured name with family, given, prefix, suffix, period
  • Address - Postal address with line, city, state, postalCode, country
  • ContactPoint - Phone, email, or other contact details
  • Identifier - Business identifier with system and value
  • CodeableConcept - Coded value with text fallback
  • Coding - Reference to a code in a terminology system
  • Quantity - Measured amount with units
  • Reference - Reference to another resource
  • Period - Time range with start and end

Identifier Systems

Identifiers in FHIR use a system/value pair to ensure global uniqueness:

  • System - A URI that defines the namespace for the identifier (e.g., http://hospital.example.org/patients)
  • Value - The actual identifier within that system (e.g., 123456)

Common identifier system patterns include:

  • OIDs - urn:oid:2.16.840.1.113883.4.1 (US Social Security Numbers)
  • URIs - http://hl7.org/fhir/sid/us-ssn (more readable alternative)
  • Organization-specific - http://hospital.example.org/mrn (Medical Record Numbers)

Resource References vs Contained Resources

When one resource needs to refer to another, you have two options:

References (Preferred) - Point to resources stored independently on the server:

// Reference to a separately stored Patient
observation.setSubject(new Reference("Patient/123"));

Contained Resources - Embed the referenced resource within the parent:

// Contained resource - embedded within the parent
Patient inlinePatient = new Patient();
inlinePatient.setId("#pat1");
observation.addContained(inlinePatient);
observation.setSubject(new Reference("#pat1"));

Use references when the resource has independent existence and may be shared. Use contained resources only when the referenced resource has no independent identity, cannot be accessed outside the parent, or when you need to include a snapshot of data at a point in time.

Resource Lifecycle

Understanding the lifecycle of FHIR resources helps in designing robust applications:

  1. Create - POST to [base]/[ResourceType] creates a new resource; the server assigns an ID
  2. Read - GET from [base]/[ResourceType]/[id] retrieves a specific resource
  3. Update - PUT to [base]/[ResourceType]/[id] replaces the entire resource
  4. Delete - DELETE to [base]/[ResourceType]/[id] removes the resource
  5. History - GET from [base]/[ResourceType]/[id]/_history retrieves all versions

Each modification creates a new version. The server tracks versionId and lastUpdated in the resource's meta element.

Bundles and Transactions

When creating multiple related resources, FHIR provides the Bundle resource for atomic operations:

Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);

// Add a Patient
Bundle.BundleEntryComponent patientEntry = bundle.addEntry();
patientEntry.setResource(patient);
patientEntry.getRequest()
    .setMethod(Bundle.HTTPVerb.POST)
    .setUrl("Patient");

// Add an Observation referencing the patient
Bundle.BundleEntryComponent obsEntry = bundle.addEntry();
obsEntry.setResource(observation);
obsEntry.getRequest()
    .setMethod(Bundle.HTTPVerb.POST)
    .setUrl("Observation");

// Execute as a transaction - all succeed or all fail
Bundle response = client.transaction().withBundle(bundle).execute();

Transaction bundles ensure atomicity: either all operations succeed, or none do. This is essential when creating resources that depend on each other.

Prerequisites

Ensure your environment is ready before proceeding:

  • 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

“Interoperability is key to advancing healthcare technology. FHIR resources provide the building blocks for standardized healthcare data exchange that improves patient care through better information flow.”

Step 1 of 5: Import Required Classes

Before creating a FHIR resource, we need to import the necessary classes from the HAPI FHIR library. These classes will allow us to define and manipulate FHIR resources in our Java application. Start by creating a new Java class file called `PatientResourceCreator.java` in your project:

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.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;

public class FhirCreateResources {
    public static void main(String[] args) {
        // We'll implement our code here
    }
}

These imports include the core FHIR resource models, such as `Patient`, `HumanName`, `Identifier`, and `ContactPoint`, which we will use to create and manage patient data.

Step 2 of 5: Create a FHIR Resource

Now, let's create a new patient resource. A patient resource contains all the necessary information to represent an individual receiving healthcare services. The code snippet below shows how to create a `Patient` resource and populate it with basic information such as name, identifier, and contact details:

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.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;

public class FhirCreateResources {
    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);

        System.out.println("Creating a new Patient resource...");

        // Create a new Patient resource
        Patient patient = new Patient();

        // Add an identifier
        patient.addIdentifier()
                .setSystem("http://hospital.smarthealthit.org")
                .setValue("123456");

        // Add a name using method chaining
        patient.addName()
                .setUse(HumanName.NameUse.OFFICIAL)
                .setFamily("Doe")
                .addGiven("John")
                .addGiven("Adam")
                .addPrefix("Mr.");

        // Set gender
        patient.setGender(Enumerations.AdministrativeGender.MALE);

        // Set birth date using java.time (thread-safe)
        LocalDate birthDate = LocalDate.parse("1980-01-01", DateTimeFormatter.ISO_LOCAL_DATE);
        patient.setBirthDate(Date.from(birthDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));

        // Set active status
        patient.setActive(true);

        // Add address using method chaining
        patient.addAddress()
                .setUse(Address.AddressUse.HOME)
                .setType(Address.AddressType.PHYSICAL)
                .addLine("123 Main St")
                .setCity("Metropolis")
                .setState("NY")
                .setPostalCode("10001")
                .setCountry("USA");

        // Add contact information
        patient.addTelecom()
                .setSystem(ContactPoint.ContactPointSystem.PHONE)
                .setValue("555-123-4567")
                .setUse(ContactPoint.ContactPointUse.HOME);

        patient.addTelecom()
                .setSystem(ContactPoint.ContactPointSystem.EMAIL)
                .setValue("[email protected]")
                .setUse(ContactPoint.ContactPointUse.WORK);

        // Display the patient details locally
        System.out.println("Patient resource created locally:");
        if (patient.hasName()) {
            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);
        }
        System.out.println("  Gender: " + patient.getGender());
        System.out.println("  Birth Date: " + birthDate);

        // Print the resource as JSON
        String encoded = ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient);
        System.out.println("Created Patient Resource:\n" + encoded);
    }
}

This code creates a patient named John Adam Doe with an identifier, contact details, and address information. It uses method chaining for a cleaner, more readable code style. The patient resource is then printed in JSON format, allowing you to visualize the structure and content of the resource.

Step 3 of 5: Persist the Resource on the FHIR Server

Once the resource is created, it's time to persist it on the HAPI FHIR server. Persisting a resource means sending it to the server where it can be stored, retrieved, and managed. The following code snippet shows how to send the patient resource to the FHIR server:

// Send the Patient to the FHIR server
System.out.println("Sending Patient resource to FHIR server...");
MethodOutcome outcome = client.create()
        .resource(patient)
        .execute();

// Check if the outcome has an ID before accessing it
if (outcome.getId() == null) {
    System.err.println("Error: Failed to create patient - no ID returned from server.");
    return;
}

String patientId = outcome.getId().getIdPart();
System.out.println();
System.out.println("Patient created successfully on the FHIR server!");
System.out.println("  Resource ID: " + patientId);
System.out.println("  Resource URL: " + fhirServerUrl + "/Patient/" + patientId);

This code snippet connects to your FHIR server, sends the patient resource, and confirms successful creation. The `client.create().resource(patient).execute()` method is used to persist the resource, which corresponds to an HTTP POST request in RESTful terms. The null check ensures we handle cases where the server doesn't return an ID.

Step 4 of 5: Verify Resource Creation

After persisting the resource, it's essential to verify that it was created correctly. You can use the client to retrieve the resource by its ID. Here's how you can retrieve and verify the patient resource:

// Verify the creation by reading it back
System.out.println("Verifying creation by reading the patient back...");
Patient retrievedPatient = client.read()
        .resource(Patient.class)
        .withId(patientId)
        .execute();

System.out.println();
System.out.println("Retrieved Patient:");
System.out.println("  ID: " + retrievedPatient.getId());

if (retrievedPatient.hasName() && !retrievedPatient.getName().isEmpty()) {
    HumanName name = retrievedPatient.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);
}

System.out.println("  Gender: " + retrievedPatient.getGender());
System.out.println("  Active: " + retrievedPatient.getActive());

This code retrieves the patient resource by its ID and displays the key details. The null-safe checks using `hasName()` and `hasGiven()` ensure we handle cases where the patient may not have name data populated.

Step 5 of 5: Complete Working Example

Below is the complete working example that matches the code in the GitHub repository. This example includes proper error handling and null-safe access patterns:

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.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;

/**
 * This tutorial demonstrates how to create FHIR resources using Java and the HAPI FHIR library.
 * We'll focus on creating a Patient resource, which is one of the most commonly used
 * resources in FHIR.
 */
public class FhirCreateResources {

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

        System.out.println("FHIR Java SDK - Creating FHIR Resources Tutorial");
        System.out.println("================================================");
        System.out.println();

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

            // Step 1: Create a new Patient resource
            System.out.println("Step 1: Creating a Patient resource locally...");
            Patient patient = new Patient();

            // Add an identifier
            patient.addIdentifier()
                    .setSystem("http://hospital.smarthealthit.org")
                    .setValue("123456");

            // Add a name
            patient.addName()
                    .setUse(HumanName.NameUse.OFFICIAL)
                    .setFamily("Doe")
                    .addGiven("John")
                    .addGiven("Adam")
                    .addPrefix("Mr.");

            // Set gender
            patient.setGender(Enumerations.AdministrativeGender.MALE);

            // Set birth date using java.time (thread-safe)
            LocalDate birthDate = LocalDate.parse("1980-01-01", DateTimeFormatter.ISO_LOCAL_DATE);
            patient.setBirthDate(Date.from(birthDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));

            // Set active status
            patient.setActive(true);

            // Add address
            patient.addAddress()
                    .setUse(Address.AddressUse.HOME)
                    .setType(Address.AddressType.PHYSICAL)
                    .addLine("123 Main St")
                    .setCity("Metropolis")
                    .setState("NY")
                    .setPostalCode("10001")
                    .setCountry("USA");

            // Add contact information
            patient.addTelecom()
                    .setSystem(ContactPoint.ContactPointSystem.PHONE)
                    .setValue("555-123-4567")
                    .setUse(ContactPoint.ContactPointUse.HOME);

            patient.addTelecom()
                    .setSystem(ContactPoint.ContactPointSystem.EMAIL)
                    .setValue("[email protected]")
                    .setUse(ContactPoint.ContactPointUse.WORK);

            System.out.println("Patient resource created locally:");
            if (patient.hasName()) {
                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);
            }
            System.out.println("  Gender: " + patient.getGender());
            System.out.println("  Birth Date: " + birthDate);
            System.out.println();

            // Step 2: Send the Patient to the FHIR server
            System.out.println("Step 2: Sending Patient resource to FHIR server...");
            MethodOutcome outcome = client.create()
                    .resource(patient)
                    .execute();

            // Check if the outcome has an ID before accessing it
            if (outcome.getId() == null) {
                System.err.println("Error: Failed to create patient - no ID returned from server.");
                return;
            }

            String patientId = outcome.getId().getIdPart();
            System.out.println();
            System.out.println("Patient created successfully on the FHIR server!");
            System.out.println("  Resource ID: " + patientId);
            System.out.println("  Resource URL: " + fhirServerUrl + "/Patient/" + patientId);
            System.out.println();

            // Step 3: Verify the creation by reading it back
            System.out.println("Step 3: Verifying creation by reading the patient back...");
            Patient retrievedPatient = client.read()
                    .resource(Patient.class)
                    .withId(patientId)
                    .execute();

            System.out.println();
            System.out.println("Retrieved Patient:");
            System.out.println("  ID: " + retrievedPatient.getId());

            if (retrievedPatient.hasName() && !retrievedPatient.getName().isEmpty()) {
                HumanName name = retrievedPatient.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);
            }

            System.out.println("  Gender: " + retrievedPatient.getGender());
            System.out.println("  Active: " + retrievedPatient.getActive());

        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            e.printStackTrace();
        }

        System.out.println();
        System.out.println("Tutorial completed.");
    }
}

This complete example demonstrates the full workflow of creating a FHIR Patient resource, persisting it to a FHIR server, and verifying the creation. The code includes proper error handling with try-catch blocks and null-safe access patterns using `hasName()` and `hasGiven()` checks.

Creating Other FHIR Resources

The same pattern can be used to create other types of FHIR resources. Let's look at a brief example of creating an Observation resource, which represents clinical observations such as vital signs or lab results:

private static Observation createObservation(String patientId) {
    Observation observation = new Observation();
    
    // Set status
    observation.setStatus(Observation.ObservationStatus.FINAL);
    
    // Set category (vital-signs)
    CodeableConcept category = new CodeableConcept();
    category.addCoding()
        .setSystem("http://terminology.hl7.org/CodeSystem/observation-category")
        .setCode("vital-signs")
        .setDisplay("Vital Signs");
    observation.addCategory(category);
    
    // Set code (blood pressure)
    CodeableConcept code = new CodeableConcept();
    code.addCoding()
        .setSystem("http://loinc.org")
        .setCode("85354-9")
        .setDisplay("Blood pressure panel with all children optional");
    observation.setCode(code);
    
    // Set subject (patient reference)
    observation.setSubject(new Reference("Patient/" + patientId));
    
    // Set effective date/time
    observation.setEffective(new DateTimeType(new Date()));
    
    // Add component for systolic
    Observation.ObservationComponentComponent systolic = observation.addComponent();
    CodeableConcept systolicCode = new CodeableConcept();
    systolicCode.addCoding()
        .setSystem("http://loinc.org")
        .setCode("8480-6")
        .setDisplay("Systolic blood pressure");
    systolic.setCode(systolicCode);
    
    systolic.setValue(new Quantity()
        .setValue(120)
        .setUnit("mmHg")
        .setSystem("http://unitsofmeasure.org")
        .setCode("mm[Hg]"));
    
    // Add component for diastolic
    Observation.ObservationComponentComponent diastolic = observation.addComponent();
    CodeableConcept diastolicCode = new CodeableConcept();
    diastolicCode.addCoding()
        .setSystem("http://loinc.org")
        .setCode("8462-4")
        .setDisplay("Diastolic blood pressure");
    diastolic.setCode(diastolicCode);
    
    diastolic.setValue(new Quantity()
        .setValue(80)
        .setUnit("mmHg")
        .setSystem("http://unitsofmeasure.org")
        .setCode("mm[Hg]"));
    
    return observation;
}

To create the Observation on the FHIR server, you would use the same client approach as with the Patient resource:

// After creating a patient and getting its ID
// Create an observation for this patient
Observation bloodPressure = createObservation(patientId);

// Create the observation on the FHIR server
MethodOutcome obsOutcome = client.create()
    .resource(bloodPressure)
    .execute();

System.out.println("Observation created with ID: " + obsOutcome.getId().getValue());

Conclusion

Congratulations! You have successfully created your first FHIR resource using Java and the HAPI FHIR library. In this tutorial, we covered the essential steps to create, persist, retrieve, and handle errors when working with FHIR resources. This knowledge forms the foundation for building more complex and feature-rich healthcare applications.

In the next tutorial in this series, we will explore how to update FHIR resources, enabling you to modify existing healthcare data stored on your FHIR server.