The Architecture and the (FHIR) Message
March 24, 2015 2 Comments
So now that we’ve established how we intend to build our model from HL7 v2 messages, let’s turn our attention to architectural issues. We’ll need to look at how FHIR thinks about managing messages – both in terms of the structure and content of a FHIR message, and also how the server should process those messages.
First up, lets think about Systems Architecture. We’ve actually got some prior art on this, because we talked previously about FHIR messages in the context of Observations, and most of that discussion is still relevant. If you want to refresh your memory, the links are here and here.
Here’s the architecture diagram that we used then:
We’ll use a similar architecture here (though the resources will be different), but we’ll also add an EMPI – or Electronic Master Patient Index to our solution. This is a component that manages patient identity – especially across multiple organizations that use different identifiers for the same person. (In IHE XDS it’s referred to as the ‘patient identity source’). It means that a single person can have multiple identifiers, and we only need one of them to get all the data for that person – regardless of which identity we specify. In practice, this is a very complex piece of software, we we’ll just think of it as a black box that has magic inside…
Since we control the whole system (ie the Integration Engine, the EMPI and the FHIR server), we can make a few assumptions and optimizations about processing and resource resolution – specifically we won’t perform any lookups on patient, provider or encounter in the Integration Engine. (The ‘Message Processor’ in the diagram).
The main reason not to do this is performance and complexity – there is an overhead in querying external servers for resources, so we’re going to have the mailbox server do this directly – i.e. as part of server processing.
In addition, while we’re really interested in identifying the patient – we’re less interested in identifying the various Practitioners associated with the encounters. We will have their names from the v2 messages, but we don’t really need to associate them with a specific FHIR resource on our server. The down-stream impact of this is that we won’t be able to answer queries like “Tell me all the encounters where Dr. X was the Attending Physician” – but we don’t want to do that here. (This may be different to your requirements of course, in which case Practitioner identification becomes more important).
So for the patient, we’ll put the Patient resource (along with identifiers) in the bundle, and require that the server manage the identity resolution directly.
For the practitioner, we have a couple of options:
- We could simply put their display name in the ‘display’ property of the reference, and leave the reference url empty.
- We could create a contained Practitioner resource with an internal reference.
The first option does seem a bit lazy, so we’ll be good FHIR citizens and use a contained resource. (We’d be even gooder if we resolved the reference, but we’ve already decided we don’t need to do that).
We also won’t look up the encounter. As you will see, we can delegate that task to the server also.
So with that background sorted, it’s time to think about what FHIR messages are we going to need. Like v2 messages, we will differentiate them based on their event code – but the actual codes in FHIR are not the same as the v2 ones. Here’s the current list – though the list is likely to change as we gain experience with messaging based implementations
Looking at the list of v2 messages we’ve decided to support, we can group them as follows (refer to the previous post for a description of each v2 message):
Purpose | V2 messages | Resources in the FHIR Message | Event |
Create an encounter | A01, A04, A05 | MessageHeader (1..1)Patient (1..1)
Encounter (1..1) Condition (0..*) AllergyIntolerance (0..*) Procedure (0..*) Observation (0..*) |
Admin-Notify |
Update encounter details | A02, A03, A06, A07, A11, A12, A13, A38, A45, A50 | MessageHeader (1..1)Patient (1..1)
Encounter (1..1) |
Admin-Notify |
Update clinical data | A08, A28 | MessageHeader (1..1)Patient (1..1)
Encounter (1..1) Condition (0..*) AllergyIntolerance (0..*) Procedure (0..*) Observation (0..*) |
Admin-Notify(?Clinical-Notify) |
Change/Update Patient Identity | A31, A37, A40 | MessageHeader (1..1)Patient (1..2) | Admin-Notify (A31)Patient-link (A40)
Patient-unlink (A37) |
(Note that the Clinical-Notify is not an official event – we’ll talk about that in a minute)
So the Integration Engine will create FHIR messages from the v2 messages, and will use the event types and resources indicated in the table. Lets take a closer look at those resources (we’ll focus on the ‘workflow-y’ aspects rather than the detailed contents – that can come later).
Because we’re not doing any resource resolution in the Integration Engine, all of these resources will have a ‘client’ id (cid:) in the message, which generally indicates to the server that these are new (or at least unidentified) resources. As we will see, our mailbox server won’t blindly save them, but will apply logic based on the event type – and also using the ‘conditional update’ feature of FHIR.
The MessageHeader manages the ‘transport level’ interaction between sender and recipient. This is described in some detail in the spec, and it covers things like dates, who created the message, from which system, where it is to go, the event that caused it to be generated, the message ids to be used to co-ordinate acknowledgements and re-sends, and so forth. It is equivalent to the v2 MSH segment with a couple of extra fields. Every FHIR message (which is, of course, a FHIR bundle) will have one of these.
The Patient is also included in every message. It will contain at least the identifier from the source system, but may have name and other demographics as well (and there’s something a bit special about the ID which we’ll cover in a moment – after we’ve discussed the encounter). For most of the messages its main purpose is to establish the patient identifiers, but for last category in the table above it is going to be a bit special, as these messages won’t change the information in our model, but rather will update the EMPI that is managing patient identity.
The A31 (Update patient Information) is reasonably straightforward – it adds the patient (including their identifier) to the EMPI so that future messages can refer to that identifier. In our case we’re going to ignore any clinical data that is in the message as it’s not related to an encounter – though we re-visit that question at the end of this series of posts. (I do wonder if there should be a specific ‘Patient-Update’ event rather than the generic ‘Admin-Update’ but we can live without it for now)
The A40 (Merge Patients) and A37 (Unlink Patients) are also instructions to the EMPI, and they handle situations where patients have been incorrectly identified – or have been assigned temporary identifiers (common in Emergency Departments for unconscious patients).
As an aside, temporary identifiers can be a real issue – especially in situations where a temporary identifier can be re-used after the ‘real’ identifier has been established – hard to believe, but apparently true.
The message will contain 2 Patient resources (each with the required identifier) and the MessageHeader.event indicates the action to be taken. For the ‘merge’ (A40) event, the Patient that is being replaced has a link property that refers to the Patient replacing it, with a link type of ‘replace’. The EMPI can then perform the required update internally.
For the un-link operation, the EMPI will simply remove the relationship.
Incidentally, there’s no real reason why you couldn’t have more than 2 Patient resources in the link/unlink messages – in effect what you’re saying is that these resources (however many there are) are the same (or different) person.
The Encounter resource represents the visit. As we stated above, we’re not doing any lookups in the message conversion, so it will always be a ‘new’ resource in the message (i.e. like all the resources in the message it will have a cid: id) and it will be up to the server to decide how to manage them.
The visit number from PV1-19 will be the Encounter.identifier (Remember our assumption of this in the previous post). An Identifier datatype has a number of useful properties that we could take advantage of – but we’ll probably just use the value (which will have the contents of PV1-19) and the system – which we can create by concatenating the sending facility (MSH-4) and sending application (MSH-3). More elegant solutions – eg using the assigner property are possible of course.
However, it’s not enough to just include the identifier in the message. Because it will have a cid: identifier (ie we haven’t specified the actual encounter ID), by default the server will just create a new one each time. We need to be able to tell the server how to determine whether this is a new or an updated encounter. The solution is to use ‘conditional updates’, which is a way of informing the server how to tell if a resource is a new one, or one to be updated. The mechanism is slightly different in DSTU-1 & DSTU-2. In DSTU-1 you add a special ‘search link’ to the bundle entry with the parameters to use to determine whether to update or create – eg:
<link href="http://localhost/Patient?[parameters]" rel="search"/>
In DSTU-2 it’s been more formally represented with the addition of conditional updates where you can specify the criteria to use to determine whether this is an update or a create in the id itself. In our case, we simply need to add the existing identifier (which is in the PV1-19 field) as a query parameter on the id, so the id of the Encounter resource in the bundle would be something like:
cid://encounter1?identifer={system}|{identifier}
We should also do this even when we do want a new encounter to be created – A01, A04 & A05. The reason is that if we were to get multiple encounters with the same identifier, then we will be unable to determine which one to update. In fact the conditional update specifies that if there are 2 or more resources matching the query parameter, then the update should fail.
Also, for an A50 (change identifier) we must be careful to specify the old identifier (from MRG-5) in this id (Interestingly, this is one situation where out-of-sequence messages are going to be a real headache – managing this is left to the implementer 🙂 )
The other properties of the encounter will vary according to the message type – for example an A03 (discharge) will set the status to finished whereas many of the others will change the encounter type.
And now we can talk about the Patient id – it’s going to be the same, ie something like:
cid://patient?identifer={system}|{identifier}
The Condition resource represents the diagnosis for this visit. It is the same resource as used for a problem in a problem list, but the category is set to ‘diagnosis’. We’ll set the encounter property to refer to the encounter where it was asserted – this will allow us to provide views like the number of times that people were hospitalized for specific conditions.
There are 2 possible segments in the v2 message that can contain diagnosis information – the DG1 (Diagnosis Information) and the DRG (Diagnosis Related Group). These are both defined in chapter 6 of the v2 spec (which is all about financial information). We’ll only use the DG1 segments here, as the DRG is more about reporting and billing than clinical use.
As we decided earlier, we’re only going to update diagnoses related to a visit in 2 situations:
- When the encounter is first created
- When a specific ‘Update Patient Information’ message is received.
In addition, we’re going to treat updates as snapshots – we’ll assume that each update message has the complete list of diagnoses, so that means that when the server processes an update message it will first remove all current diagnoses for that visit (encounter), and then create new ones corresponding to the DG1 segments.
The Procedure resource is processed in a similar way to the diagnosis, being created by A01 messages, and updated as a snapshot by A08 and A28. It also has an encounter reference that we will set.
The Observation is also a snapshot based update, but we’re going to need to add an extension to link it back to its encounter. To tell the truth, I’m not entirely sure what observation based information we are going to get in ADT messages, but it is in there, so we may as well use it. It will need an extension to refer back to the encounter that created it.
So that covers off the resources – at least at a high level, we’ll go into more detail in the next post in this series. The last thing to think about before wrapping up this post is how the server will process these FHIR messages.
We’re using HTTP as the transport mechanism, and the endpoint of the server that processes the FHIR messages is called the mailbox. The FHIR specification doesn’t dictate how messages should be processed by the server (in the same way as the v2 specification doesn’t either) because that will be quite different depending on the nature and purpose of the application receiving the messages, so it’s worth thinking how that might happen in our case. (Of course the specification does go into detail about how the sender & the mailbox should interact)
It is the MessageHeader.event code that is the main trigger for determining the nature of server-side processing.
If the code is patient-link or patient-unlink, then the EMPI will be updated as we discussed above.
For admin-notify messages we have 3 main actions we could perform:
- Create a new Encounter and related clinical resources
- Update the Encounter
- Update the clinical resources
The last option does present some issues. The A08 message that is its source is all about changing patient data rather than encounter data. So; we need to have the Encounter resource in the FHIR message so we know which clinical resources to update, but we don’t want to update the encounter itself – which, by default, the server would do. One solution is to define our own event code, which we’ll call ‘clinical-notify’. We’re allowed to do this, because the MessageHeader.event binding has a type of incomplete – meaning we SHOULD use one of the defined codes, but we don’t HAVE to. The downside is that this is an event that only our server will recognize (unless this event type is added to the spec) so we’ve lost some interoperability. But in this case we’re the only consumer so that’s acceptable. (As an aside, we should raise a change request in the spec to suggest that this event type is added to the spec – if it’s an issue for is, it will likely be an issue for others).
So for admin-notify:
- Sender information and dates will come from the MessageHeader
- The id for the encounter will include the identifier (conditional updates), so it can determine whether to create a new encounter, or update an existing one.
- If there are clinical resources in the message, then they will be updated as a snapshot (against the encounter).
And for our very own clinical-notify (A08, A28):
- Sender information and dates will come from the MessageHeader
- The id for the encounter will include the identifier (conditional updates). There should be an existing encounter (which we won’t update) – if there isn’t then we’ll raise an error.
- Any clinical resources in the message will be updated as a snapshot (against the encounter).
Well, that’s quite enough for now – congratulations if you made it this far! Next post we’ll look in more detail at the mappings between the data from the v2 messages, and which resource properties they will update. We’ll also think about some other v2 messages we could consume (other than ADT) in our quest to generate useful clinical summaries.
In DSTU2 (per statement from Grahame) cid: doesn’t work.. no clue as to whether that’s a temporary thing or not, we’ll have to wait and see.
As such it may be up to the integration engine to get hold or the URI of the resource that matches the search-criteria taken from the v2 message, and use that URI as a resource-reference in the translated version of a v2-message, instead of doing a straight transformation of v2 data to a resource.
For now that may be a wise thing to do anyway, given that current FHIR servers are unlikely to support messaging any time soon..