- Introduction
- Do not model your interfaces on HL7’s structure
- A rough example of an interface schema
- Create your organization’s standard module
- Overriding functions in your standard module
- Use global policies for retry and error handling
- More Information
Introduction
We believe that the most effective way to interface with HL7 is to develop some simple templates that can easily be adapted to develop new interfaces.
If you have a lot of interfaces to deal with then you cannot get better advice than from Anthony Julian, the HL7 infra-structure and Messaging Co-Chair:
“Scripting is the only answer when you have a large quantity of interfaces to handle”
Anthony knows his onions. This is Anthony’s personal opinion but keep in mind he and his team at the Mayo Clinic processed over 1.3 billion HL7 messages last year. His opinion is worth listening to. Any organization that has grasped the concept of how to cost effectively integrate with HL7 will usually go through the process of building a ‘standard HL7 template’. This should be a combination of:
- An initial starting vanilla interface file.
- A library of standard re-usable modules to handle common scenarios.
The concept isn’t difficult. Doing it well is the challenge. From my observations there is great level of variability in how well organizations integrate with HL7. Some can pull on new systems within hours others turn each interface into in a marathon project. My goal here is to give good ideas on how your organization could be more efficient by having a better standard template.
Eliot Muir – CEO of iNTERFACEWARE
Do not model your interfaces on HL7’s structure [top]
HL7 has a very non-orthogonal design. If you are unclear what I mean by orthogonal please read my article A,B,C’s, Kanji and Orthogonality which explains the concept with an everyday example. The way information is lumped together into ADT events is very arbitrary. It’s a reflection of the history of the protocol and what it replaced, namely a bunch of proprietary ad-hoc batch export data mechanisms. See my article on how the cultural legacy of level 7 impacts HL7 in a negative way. Really in an ideal world interfacing would be a lot simpler and more modular. There would be restful APIs that one could use to query for discrete small amounts of data like:
- Patient demographics
- Events pertaining to patients like registration, transfers, admits.
- Insurance provider information for a given patient.
- Drug orders for a patient.
etc. Blobbing all this data together in a big messages in ambiguous ways to arbitrary broadcast events like ADT^A01 Admit, ADT^A04 register etc., doesn’t make a lot of sense in todays world. It’s an historical legacy. Unfortunately it is the reality of the current landscape and will be for many years to come since the oldest most grungy systems are those which are updated the least often. Processing HL7 almost always means maintaining your own database which is effectively mirroring some subset of the information being sent to you by the other vendor system. However just because the HL7 protocol is structured this way doesn’t mean that your application has to inherit all the headaches for it’s own internal APIs. Whether you are using staging tables or if you have web service style APIs into your application you’ll be much more efficient at interfacing if you take the time to decompose your targets into discrete logical pieces rather than modelling your interfaces on the HL7 spec itself. It’s a very common mistake that I see with a lot of customers where they base their internal interfaces on HL7, that’s just increasing their costs per interface.
A rough example of an interface schema [top]
This a question that new customers often face when doing integration work. This is a simple schema I offer as an example, feel free to use it as a starting point for considering your own schema design. This is a work in progress, it’s not by any means finished. If you are curious in seeing this go further let me know.
Globally Unique Identifiers for Primary Keys
Using globally unique identifiers can greatly simplify your data model. These are guaranteed to be unique across the entire database. The beauty of this approach is that you can now have say a Note table which can associate notes with anything, whether it’s a person, company, lab result etc. It allows a much smaller data model.
Person
I find it’s more effective to define staff, patients, next of kin just as a person and then you define relationships between people. It makes for a smaller data model. Personally I like just:
- Guid: ID for this person
- Sex: ‘M’, ‘F’, ‘U’, ‘O’
- Dob: date of birth timestamp
- Dod: date of death timestamp
- MaritalStatus: ‘M’, ‘S’, ‘U’
ExternalId
The ExternalId table defines all the external identifiers for any object. I like to use this to cover everything like:
- Id’s from other systems
- Can be staff id etc.
- SSN: Social Security Number
- Driver’s license
- Insurance plan IDs, i.e., say what ID number State Farm has for my health insurance.
- Group Insurance Number
The columns in this table are:
- ObjectGuid: primary key of the person, company or other thing the ID relates to.
- Id: the Id
- IdType: the type of ID, i.e., ‘SSN’, “DriverLicense’, ‘AcmeMrn’, ‘insurance_plan’, ‘group_insurance_number’
Address
The address of a person, company etc.
- Guid: of the address
- ObjectGuid: primary key of the associated person, company etc.
- Street
- Other
- City
- Zip
- Country
- AddressType: ‘home’, ‘business’, ‘birth’ (where they were born),’death’
Phone
- Guid: of the phone, allows notes to be associated with it
- ObjectGuid: what the phone number refers to, could be a company, person etc.
- CountryCode
- AreaCode
- Number
- Extension
- PhoneType: ‘home’, ‘business’, ‘contact’, ‘call_back’
Phone numbers can be associated with people, insurance companies etc.
PersonName
- Guid
- ObjectGuid: primary key of the associated person
- FirstName
- MiddleName
- LastName
- NameType: i.e., ‘alias’, ‘maiden’, ‘primary’
Health data often contains a lot of extra fields like Degree, Suffix etc. I think that it’s often redundant and better placed in a note associated with a person if the data actually matters.
Note
Notes can be related to any object, be it a person, company, lab result, sub lab test etc.
- Guid: the guid of the note
- ObjectGuid: what the note relates to: Notice it can be related to a person, company, lab result etc.
- Timestamp: when the note was created.
- Text: text of the note
- Author: Guid of the person that created the note, may be blank
Location
This is more intended for use in pinpointing a location within a hospital
- Guid
- Poc: point of care
- Room
- Bed
- Facility
- Building
- Floor
PersonLocation
- Guid: Guid of this record, useful for attaching a Note to this record.
- PersonGuid
- LocationGuid
- StartTimeStamp
- EndTimeStamp
- LocationType: ‘prior’, ‘temporary’, ‘current’, ‘pending’, ‘prior_temporary’
Relationship
This defines a relationship between two things. People and things can have multiple relationships.
- Guid: allows association of Note
- PrimaryGuid
- SecondGuid
- StartTimeStamp: can be blank for permanent relationships like kin
- EndTimeStamp: can be blank if no end.
- Relationship: ‘mother’, ‘father’, ‘referring_doc’, ‘attending_doc’, ‘consulting_doc’, ‘admitting_doc’, ‘guarantor’, ‘insurance_company’, ’employee’, ’employer’, ‘contact_person’, ‘observation_collector’, ‘copy_results_to’, ‘principle_result_interpreter’, ‘assistant_result_interpreter’, ‘technician’, ‘transcriptionist’, ‘diagnosing_doctor’, ‘patient’
Visit
- Guid: of the visit, this allows Notes and External keys to be associated with it.
- ObjectGuid: of the person associated with the visit
- StartTimeStamp: admission time
- EndTimeStamp: discharge time
Organization
These can be insurance companies, healthcare providers, employer.
- Guid: allows association with addresses, phone numbers people.
- Name: name of the company
InsurancePlan
This is a relatively sketchy area of the data model since it’s so ambiguous. I really have not fleshed it out.
- Guid: primary key of plan, allows notes and External Keys to be associated with it.
- StartTimeStamp: effective start date of the plan
- EndTimeStamp: expiration date of the plan
- InsuredGuid: the Guid of the Person who has the plan, i.e., parent who has the plan.
To associate the employer with a plan I would use the Relationship table. The Person, Relationship and Organization tables provide enough of a data-structure to get most of the information in to it.
Order
- Guid: allows notes, phone numbers to be associated with the order
- PatientGuid: the person for which the order is being made for
- PlacerGuid: the person or organization asking for the order
- FillerGuid: the person or organization that fulfils the order
- EnteredGuid: who entered the order
- VerifiedGuid: who verified the order
- EntererLocation: location of who entered the order
- OrderStatus: status of the order
- EffectiveTime
I left out the Placer Group Number and action by. The callback phone number can be associated with the order
ObservationRequest
- Guid
- PlacerGuid: the person or organization asking for the order
- FillerGuid: the person or organization that fulfils the order
- RequestDateTime
- ObservationDateTime
- ObservationEndDateTime
Observation collectors (people) can be related using the Relationship table. Order call back numbers can likewise be associated using the phone table. I didn’t create places for the results report status changing and so on. Transportation mode seems like something that belonged in a note. Haven’t covered the reason for the study. Did not cover the number of sample containers. Associated people like the principle result interpreter, assistant result interpreter, technician and transcriptionist seem best implemented using the Relationship table. I have not covered scheduling information here.
Diagnosis
By default we’ll assume the use of ICD9 codes for diagnosis codes.
- Guid: of the observation itself, allows association of comments.
- Code
- TimeStamp
- Confidential: true/false
I have not really covered the diagnosis related group, major category, outlier type, outlier days, outlier costs or the attestation date/time.
Observation
This is rather incomplete. Really the valid data here will vary greatly depending on the nature of observation, a.k.a lab result.
- Guid: of the observation, allows notes to be associated
- Value
- Units
- TimeStamp: time of the observation
- Abnormal: true/false
- LowRange
- HighRange
The responsible observer can be associated with the Relationship table. Likewise for the producer.
Extending the Paradigm
This approach to modelling data becomes very extensible. Let’s say we want to exchange EKG data. Then it become a more straightforward model to extend. If just think of an EKG being a big old blob of data. Now we can make a web service call that can return EKG data. We might need to have the ability to break it down into pieces with compression because it’s going to be quite a large amount of data to send over the wire. But we can keep that web service cleanly specialized on just that EKG data. If each EKG get’s it’s own GUID then we can use that to associate the EKG with other data. For instance:
- The associated patient would be an entry in the Relationship table, GUID of the Person representing the patient, with the relationship being ‘patient’
- The lab techs, organization that provided the EKG, the doctor ordering it etc. all can be specified by other entries in the Relationship table.
In this manner things become very flexible and make it much easier to cover the different ways that different heath institutions work.
Data Not Covered
I have not covered data like:
- Veteran’s military status
- VIP status
- Citizenship
- Ambulatory Status
- Financial data.
- Diet
- Employment status
- Much of the potential insurance information
- Household size
Create your organization’s standard module [top]
The first step to creating a standard template is to create a shared module with your standard routines in it. If your company is called “Acme” for instance it would make sense to make a module called “acme” and then your main routine always have: local acme = require 'acme'
The idea is your standard template should have small, well defined reusable routines that can be used again and again with each interface. Each interface should only require you to write a small amount of code to address what is different for that interface rather than copy-pasting a whole load of boiler plate code. A good strategy in your template library is to break down the functions into table name spaces. For instance say your company is called Prime One then your top level nesting name could be “p1”. Then you can group utility functions under this name like this:
p1={} p1.height = {} p1.allergy = {} function p1.height.extractHeight(Msg) -- Implementation goes here return 10 end function p1.height.extractWeight(Msg) -- Implementation goes here return 10 end function p1.allergy.listAllergies(Msg) return 10 end
Because of the Translator’s code completion capabilities this makes for a very convenient library structure to use since you can type p1 and you’ll get something like this:
If the user then types a few letters so that the Translator’s deep auto-completion kicks in you’ll start to see how convenient this structure is for the person implementing the interface:
From there doing a the down arrow and return completes the full path to the function. It makes for an extremely pleasant work flow.
Overriding functions in your standard module [top]
Lua makes it very elegant to over ride functions defined within your standard template. It’s a very important concept to grasp if you want to make a standard template. Say you had a function called “MapPIDName2Patient” in your standard template and you wanted to over-ride it. All you have to do is define a new version of this function within your main file and this will replace the usage of the function defined in the standard template. Furthermore annotations make it clear which implementation is being followed since the annotation block only appears beside the function which is actually called. This will really pay off if you have taken the time to break your template into small well defined functions that handle small parts of the mapping.
Here’s an example of overriding the date format for the string:D()
function in the fuzzy date/time parser:
- We format a date using the default
string:D()
function using the default implementation of thenode:D()
time conversion function translating the format “19980210” to “1998-02-10 00:00:00”:- This the code for main:
- This annotation shows the call to the
string:D()
function in the dateparse module (for the hard-coded string date):
- This annotation shows the call to the
node:D()
function, which converts the node to a string and then calls the thestring:D()
function as above (for the HL7 date node ):
Note: This is just to show the function call, you don’t need to understand how it works.
- This the code for main:
- Now we override the
string:D()
functionstring:D()
function to format date as dd-mmm-YYYY, i.e., 10-Feb-1998:- This is the updated code for main which now includes the overriding function:
- There is no call annotation for the
string:D()
function in the dateparse module:
- This annotation shows the call to the
node:D()
function, which converts the node to a string and then calls the thestring:D()
function as above (for the HL7 date node ):
Didn’t expect that? Look closely and you will see that it is for the second call as it uses the date from the message – the call to thestring:D()
function then goes to the overriding function in the main module:
- This is the updated code for main which now includes the overriding function:
Note: We put the overriding string:D()
function for demonstration purpose only. We strongly recommend putting it a local module (if you only want to change behaviour for this channel only) or a shared module (if you want to change behavour for all channels).
This is the code if you want to try it out:
require 'dateparse' -- this function overrides the string:D() function -- in the dateparse module. This function will -- now be called instead of string:D() in the -- dateparse module -- NOTE: you can simply change the name of the -- function to DD (or whatever) to stop -- the overriding --function string:DD(fmt) -- uncomment this line and function string:D(fmt) -- comment out this line to stop overriding local t = dateparse.parse(self, fmt) return t and os.date('%d-%b-%Y', t) end function main(Data) -- using default node:D() function -- hard coded date as a string Date = "19980210" Date:D() -- this also works for date in a node tree -- using a date node from an HL7 message node tree local Msg = hl7.parse{vmd='demo.vmd', data=Data} -- convert the MSH segment (header) timestamp Msg.MSH[7][1]:D() end
Use global policies for retry and error handling [top]
If you are running a big Iguana implementation like some of our customers do then centralizing your retry logic and error handling into a common module makes a lot of sense. It takes a lot of the pain out of having to set this information on a one by one basis. Instead just set that logic in one place. For a big deployment it may make sense to really customize the error handling behaviour, i.e., call a web service, escalate the warnings if the outage exceeds a certain time threshold etc. The whole point of Iguana is that it puts you in the driving seat in terms of make Iguana work the way your organization works. See handling retry logic.
More Information [top]
Here are some further articles that may be of interest: