HL7 Programming using Java and HAPI - Creating ACK Messages

Introduction

This is part of my HL7 article series. Before we get started on this tutorial, have a quick look at my earlier article titled "A Very Short Introduction to the HL7 2.x Standard". In this tutorial, we will explore how to create ACK (Acknowledgment) messages using Java and the HAPI framework. ACK messages are fundamental to HL7 communication as they confirm receipt and processing status of messages between healthcare systems.

When a HL7 message is sent from one system to another, the receiving system typically responds with an acknowledgment message to indicate whether the message was received successfully, encountered an error during processing, or was rejected entirely. Understanding how to properly create and handle these acknowledgment messages is crucial for building robust healthcare integration solutions.

Tools for Tutorial

“The art of communication is the language of leadership.” ~ James Humes

Understanding HL7 Acknowledgment Codes

Before diving into the code, let us understand the different acknowledgment codes defined by the HL7 standard. These codes indicate the result of processing the original message:

  • AA (Application Accept) - Message processed successfully
  • AE (Application Error) - Error in message content (syntax or semantic error)
  • AR (Application Reject) - Message rejected (application cannot process)
  • CA (Commit Accept) - Message committed to safe storage
  • CE (Commit Error) - Commit error occurred
  • CR (Commit Reject) - Commit rejected

The ACK message structure consists of the following segments:

  • MSH - Message Header (mirrors original with response details)
  • MSA - Message Acknowledgment (acknowledgment code and reference to original message)
  • ERR - Error segment (optional, for error details)

Step 1 of 3 - Define the Acknowledgment Codes

First, let us create an enumeration to represent the different acknowledgment codes we will be using:

package com.saravanansubramanian.hapihl7tutorial.ack;

/**
 * Enumeration of HL7 acknowledgment codes.
 */
public enum AcknowledgmentCode {
    /** Application Accept - Message processed successfully */
    AA("AA"),
    /** Application Error - Error in message content */
    AE("AE"),
    /** Application Reject - Message rejected */
    AR("AR"),
    /** Commit Accept - Message committed to safe storage */
    CA("CA"),
    /** Commit Error - Commit error */
    CE("CE"),
    /** Commit Reject - Commit rejected */
    CR("CR");

    private final String code;

    AcknowledgmentCode(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

Step 2 of 3 - Create the ACK Message Builder

Now let us create a builder class that handles the complex task of creating properly formatted ACK messages. This builder handles swapping sender/receiver information, referencing the original message control ID, setting appropriate acknowledgment codes, and optionally adding error information:

package com.saravanansubramanian.hapihl7tutorial.ack;

import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.model.Message;
import ca.uhn.hl7v2.model.v23.message.ACK;
import ca.uhn.hl7v2.model.v23.segment.ERR;
import ca.uhn.hl7v2.model.v23.segment.MSA;
import ca.uhn.hl7v2.model.v23.segment.MSH;
import ca.uhn.hl7v2.util.Terser;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ThreadLocalRandom;

/**
 * Builder class for creating ACK (Acknowledgment) messages.
 */
public class AckMessageBuilder {

    private static final DateTimeFormatter HL7_TIMESTAMP_FORMAT =
            DateTimeFormatter.ofPattern("yyyyMMddHHmmss");

    /**
     * Creates an ACK message in response to the original message.
     */
    public static ACK createAck(Message originalMessage, AcknowledgmentCode ackCode, String textMessage)
            throws HL7Exception, IOException {

        ACK ack = new ACK();

        // Build MSH segment (response header)
        buildMshSegment(ack.getMSH(), originalMessage);

        // Build MSA segment (acknowledgment details)
        buildMsaSegment(ack.getMSA(), originalMessage, ackCode, textMessage);

        return ack;
    }

    private static void buildMshSegment(MSH msh, Message originalMessage) throws HL7Exception {
        // Use Terser for easy access to original message fields
        Terser terser = new Terser(originalMessage);

        // Standard encoding characters
        msh.getFieldSeparator().setValue("|");
        msh.getEncodingCharacters().setValue("^~\\&");

        // Swap sending and receiving (response goes back to sender)
        msh.getSendingApplication().getNamespaceID().setValue(terser.get("/MSH-5-1"));
        msh.getSendingFacility().getNamespaceID().setValue(terser.get("/MSH-6-1"));
        msh.getReceivingApplication().getNamespaceID().setValue(terser.get("/MSH-3-1"));
        msh.getReceivingFacility().getNamespaceID().setValue(terser.get("/MSH-4-1"));

        // Set response timestamp
        msh.getDateTimeOfMessage().getTimeOfAnEvent().setValue(getCurrentTimestamp());

        // Message type is ACK with the same trigger event
        msh.getMessageType().getMessageType().setValue("ACK");
        String triggerEvent = terser.get("/MSH-9-2");
        if (triggerEvent != null && !triggerEvent.isEmpty()) {
            msh.getMessageType().getTriggerEvent().setValue(triggerEvent);
        }

        // Generate unique message control ID for the ACK
        msh.getMessageControlID().setValue(generateMessageControlId());

        // Copy processing ID and version from original
        String processingId = terser.get("/MSH-11-1");
        if (processingId != null) {
            msh.getProcessingID().getProcessingID().setValue(processingId);
        }

        String version = terser.get("/MSH-12");
        if (version != null) {
            msh.getVersionID().setValue(version);
        }
    }

    private static void buildMsaSegment(MSA msa, Message originalMessage,
            AcknowledgmentCode ackCode, String textMessage) throws HL7Exception {

        Terser terser = new Terser(originalMessage);

        // Set acknowledgment code
        msa.getAcknowledgementCode().setValue(ackCode.getCode());

        // Reference the original message's control ID
        String originalControlId = terser.get("/MSH-10");
        if (originalControlId != null) {
            msa.getMessageControlID().setValue(originalControlId);
        }

        // Optional text message
        if (textMessage != null && !textMessage.isEmpty()) {
            msa.getTextMessage().setValue(textMessage);
        }
    }

    private static String getCurrentTimestamp() {
        return LocalDateTime.now().format(HL7_TIMESTAMP_FORMAT);
    }

    private static String generateMessageControlId() {
        int randomSuffix = ThreadLocalRandom.current().nextInt(1000, 9999);
        return "ACK" + getCurrentTimestamp() + randomSuffix;
    }
}

“The single biggest problem in communication is the illusion that it has taken place.” ~ George Bernard Shaw

Step 3 of 3 - Demonstrate ACK Message Generation

Now let us create a demonstration program that shows how to generate different types of ACK messages in response to an incoming HL7 message:

package com.saravanansubramanian.hapihl7tutorial.ack;

import ca.uhn.hl7v2.DefaultHapiContext;
import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.HapiContext;
import ca.uhn.hl7v2.model.Message;
import ca.uhn.hl7v2.model.v23.message.ACK;
import ca.uhn.hl7v2.parser.Parser;

import java.io.IOException;

public class HapiAckMessageExample {

    private static final HapiContext context = new DefaultHapiContext();

    // Sample incoming ADT A01 message to acknowledge
    private static final String INCOMING_MESSAGE =
            "MSH|^~\\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240115120000||ADT^A01|MSG001|P|2.3|||AL|NE|\r" +
            "EVN|A01|20240115120000|||\r" +
            "PID|1||12345^^^HOSP^MR||DOE^JOHN^A||19800101|M|||||||||||\r" +
            "PV1|1|I|ICU^101^A|||||||||||||||||||||||||||||||||||||||";

    public static void main(String[] args) {
        System.out.println("=== HAPI ACK Message Generation Example ===\n");

        try {
            Parser pipeParser = context.getPipeParser();

            // Parse the incoming message
            System.out.println("1. Incoming message to acknowledge:");
            System.out.println(formatForDisplay(INCOMING_MESSAGE));
            System.out.println();

            Message parsedMessage = pipeParser.parse(INCOMING_MESSAGE);

            // Demonstrate different ACK scenarios
            demonstrateSuccessfulAck(parsedMessage, pipeParser);
            demonstrateErrorAck(parsedMessage, pipeParser);
            demonstrateRejectAck(parsedMessage, pipeParser);

            // HAPI also provides a built-in way to generate ACKs
            demonstrateHapiBuiltInAck(parsedMessage, pipeParser);

            System.out.println("\n=== ACK Generation Complete ===");

        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
            e.printStackTrace();
        } finally {
            try {
                context.close();
            } catch (IOException e) {
                // Ignore cleanup errors
            }
        }
    }

    private static void demonstrateSuccessfulAck(Message originalMessage, Parser parser)
            throws HL7Exception, IOException {

        System.out.println("2. Creating AA (Application Accept) ACK:");
        System.out.println("   Use when message was processed successfully.\n");

        ACK ack = AckMessageBuilder.createAck(
                originalMessage,
                AcknowledgmentCode.AA,
                "Message processed successfully");

        System.out.println("   Generated ACK:");
        System.out.println(formatForDisplay(parser.encode(ack)));
        System.out.println();
    }

    private static void demonstrateErrorAck(Message originalMessage, Parser parser)
            throws HL7Exception, IOException {

        System.out.println("3. Creating AE (Application Error) ACK:");
        System.out.println("   Use when there's an error in message content.\n");

        ACK ack = AckMessageBuilder.createAck(
                originalMessage,
                AcknowledgmentCode.AE,
                "Patient ID 12345 not found in system");

        System.out.println("   Generated ACK:");
        System.out.println(formatForDisplay(parser.encode(ack)));
        System.out.println();
    }

    private static void demonstrateRejectAck(Message originalMessage, Parser parser)
            throws HL7Exception, IOException {

        System.out.println("4. Creating AR (Application Reject) ACK:");
        System.out.println("   Use when message is rejected.\n");

        ACK ack = AckMessageBuilder.createAck(
                originalMessage,
                AcknowledgmentCode.AR,
                "ADT A01 messages are not accepted by this system");

        System.out.println("   Generated ACK:");
        System.out.println(formatForDisplay(parser.encode(ack)));
        System.out.println();
    }

    private static void demonstrateHapiBuiltInAck(Message originalMessage, Parser parser)
            throws HL7Exception, IOException {

        System.out.println("5. Using HAPI's built-in generateACK() method:");
        System.out.println("   This is the recommended approach in production.\n");

        Message ack = originalMessage.generateACK();

        System.out.println("   Generated ACK (using built-in method):");
        System.out.println(formatForDisplay(parser.encode(ack)));
    }

    private static String formatForDisplay(String message) {
        return message.replace("\r", "\n   ");
    }
}

Sample Output

Running the code above will produce output similar to the following:

=== HAPI ACK Message Generation Example ===

1. Incoming message to acknowledge:
   MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240115120000||ADT^A01|MSG001|P|2.3|||AL|NE|
   EVN|A01|20240115120000|||
   PID|1||12345^^^HOSP^MR||DOE^JOHN^A||19800101|M|||||||||||
   PV1|1|I|ICU^101^A|||||||||||||||||||||||||||||||||||||||

2. Creating AA (Application Accept) ACK:
   Use when message was processed successfully.

   Generated ACK:
   MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK202501211430221234|P|2.3
   MSA|AA|MSG001|Message processed successfully

3. Creating AE (Application Error) ACK:
   Use when there's an error in message content.

   Generated ACK:
   MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK202501211430225678|P|2.3
   MSA|AE|MSG001|Patient ID 12345 not found in system

4. Creating AR (Application Reject) ACK:
   Use when message is rejected.

   Generated ACK:
   MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK202501211430229012|P|2.3
   MSA|AR|MSG001|ADT A01 messages are not accepted by this system

5. Using HAPI's built-in generateACK() method:
   This is the recommended approach in production.

   Generated ACK (using built-in method):
   MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|12345|P|2.3
   MSA|AA|MSG001

Best Practices for ACK Messages

When working with ACK messages in production systems, consider these best practices:

  • Always respond - Every incoming message should receive an acknowledgment. Failure to respond can cause the sending system to timeout and potentially resend the message.
  • Use HAPI's built-in method - In production code, prefer using message.generateACK() as it handles many edge cases automatically.
  • Include meaningful error messages - When returning AE or AR codes, provide descriptive text in the MSA-3 field to help troubleshoot issues.
  • Reference the original message - Always include the original message control ID in the MSA-2 field so the sender can correlate the response.
  • Handle exceptions gracefully - If an error occurs during message processing, return an AE or AR acknowledgment rather than no response at all.

Conclusion

In this tutorial, we explored how to create HL7 ACK messages using the HAPI framework in Java. We covered the different acknowledgment codes, created a reusable ACK message builder, and demonstrated how to generate various types of acknowledgments. Understanding ACK messages is essential for building reliable healthcare integration systems that properly communicate processing results between applications. This concludes the HL7 Java programming series using HAPI. I hope you found this series helpful in your journey to understanding HL7 programming. Please feel free to explore my other HL7 articles for more information on related topics.