FHIR Transactions

There has been a lot discussion on the Skype Implementers channel and other forums about FHIR Transactions, specifically the rules around HTTP POST and the use of Bundle.entry.fullUrl and Bundle.entry.resource.{resourceType}.id, and how servers should behave.

Readers who want to reference the relevant portions of the FHIR DSTU2 specification may refer to the Bundle resource, and the explanation of transactions.

In a FHIR POST, a resource may not contain an id element, as only a server may assign an id. In a FHIR PUT, the client should supply an id within the resource.

This is correct:

HTTP POST [base]/Patient
{
"resourceType": "Patient",
"name": [{ "family": ["Doe"], "given": ["John","James"] }],
"gender": "male",
"birthDate": "2016-01-01"
}

This is not allowed (notice the addition of an id):

HTTP POST [base]/Patient
{
"resourceType": "Patient",
"id": "1234567890",
"name": [{ "family": ["Doe"], "given": ["John","James"] }],
"gender": "male",
"birthDate": "2016-01-01"
}

In a FHIR transaction, each Bundle.entry may contain a single FHIR operation. For example, a POST or PUT (and so forth). If the operation is a POST, it seems logical that the client should not be able to provide an id within the resource (traditionally, a server generates the id on a create operation in a RESTful architecture).

But this has two issues when dealing with transactions.

First, when a resource is provided within a Bundle.entry, a fullUrl is required. See the following invariant rule:

(not(exists(f:fullUrl)) and not(exists(f:resource)))
or
(exists(f:fullUrl) and exists(f:resource)))

Second, the transaction may contain several resources that need to be linked together. For example, transferring a patient record (in this case, a simple example where there is a Patient with one Observation). Normally, the Patient requires an id before it can be referenced by an Observation. The specification, particularly this example (without resource linking), suggests that we can link resources together using the Bundle.entry.fullUrl rather than the Bundle.entry.resource.{resourceType}.id. For example:

HTTP POST [base]
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [{
  "fullUrl": "urn:uuid:9e33a01d-c35f-4920-9179-50d5b8a6f992",
  "resource": {
    "resourceType": "Patient",
    "name": [{ "family": ["Doe"], "given": ["John","James"] }],
    "gender": "male",
    "birthDate": "2016-01-01"
  },
  "request": {
    "method": "POST",
    "url": "[base]/Patient"
  }
},{
  "fullUrl": "urn:uuid:8a840627-2b30-42af-8a20-b6eea1e307b3",
  "resource": {
    "resourceType":"Observation",
    "status":"final",
    "code":{"coding":[{"system":"http://loinc.org","code":"8302-2","display":"height"}]},
    "subject":{"reference":"Patient/9e33a01d-c35f-4920-9179-50d5b8a6f992"},
    "valueQuantity":{"value":163.068,"unit":"cm","system":"http://unitsofmeasure.org","code":"cm"}                }
  },
  "request": {
    "method": "POST",
    "url": "[base]/Observation"
  }
}]
}

In this case, the fullUrl is required for each Bundle.entry (lines 6,18) because a resource is present (see the invariant rule). Normally, the Patient would also have an id because the Observation needs to reference the Patient, but in this case the Patient is referenced by urn:uuid (line 6) within the Observation (line 23).

Reading the FHIR documentation on the subject, which is spread out across two pages (Bundles and Transactions), contains language which various folks have had difficulty interpreting.

However, I think the above example is recognized as a valid use-case, and should be supported. The result being that server-side, the server continues to maintain its ability to generate the resource id values and it can maintain the resource graph (e.g. the Observation remains linked to the Patient).

Transferring a Record: $everything && transaction

The previous example is a pretty basic transaction, where the client is transferring a new patient and their associated data to another FHIR server. Let’s look at a slightly more complicated example:

  1. FHIR Client reads a Patient record using the $everything operation.
  2. FHIR Client transfers that Patient record to another FHIR server using a single transaction.

In this scenario, the client should issue a GET request for the patient (the specification allows for POST to $everything as well), read the HTTP response body, and turn around and POST the response body to the other FHIR server.

1. Fetch Patient Record

HTTP GET [base]/Patient/1234567890/$everything
HTTP/1.0 200 OK
Content-Type: application/json+fhir; charset=UTF-8
{
"resourceType": "Bundle",
"id": "everything-response",
"type": "searchset",
"entry": [{
  "fullUrl": "[base]/Patient/1234567890",
  "resource": {
    "resourceType": "Patient",
    "id": "1234567890",
    "name": [{ "family": ["Doe"], "given": ["John","James"] }],
    "gender": "male",
    "birthDate": "2016-01-01"
  }
},{
  "fullUrl": "[base]/Observation/1",
  "resource": {
    "resourceType":"Observation",
    "id": "1",
    "status":"final",
    "code":{"coding":[{"system":"http://loinc.org","code":"8302-2","display":"height"}]},
    "subject":{"reference":"Patient/1234567890"},
    "valueQuantity":{"value":163.068,"unit":"cm","system":"http://unitsofmeasure.org","code":"cm"}                }
  }
}]
}

2. Transfer Response Body as Transaction

The client just retrieved the entire Patient record. Without having to edit the response body JSON or XML at all, they should be able to turn around and POST that response as the body of a new transaction. This means no changes to:

  1. Bundle.type
  2. Bundle.entry.fullUrl
  3. Bundle.entry.resource.{resourceType}.id
  4. Bundle.entry.request

The difficulty here is that the transaction requires a request element for every Bundle.entry, which is not present in the $everything response.

One strategy is for the client to create a request for each Bundle.entry. Perhaps as a conditional create, or conditional update (both of which might require special logic on the client-side to generate the appropriate conditional search parameters), or just as an old-fashioned POST. This seems like the safest approach, but does require a lot of client-side work on creating the conditional logic.

A second strategy is for the server to make some assumptions about the request. The specification does allow support for this scenario, but does not mandate it. Specifically, see the accepting other bundle types paragraph documented within the transaction definition. Basically, in this case the server chooses to use a POST or PUT for each Bundle.entry.resource depending on whether or not the server recognizes the each Bundle.entry.resource.{resourceType}.id value or not (actually, it is NOT explicit about id value matching, instead it says “identity” which is open to interpretation).

At first glance, this seems like a great capability. However, it is potentially dangerous and destructive, so I suggest its inclusion within the specification be carefully reconsidered (or possibly just clarified if my interpretation is incorrect). These are medical records we’re talking about, not posting a new blog article or comment. If the server decides that it should use PUT instead of POST because it already has a resource of that type with the same id — how does it know that the new representation is actually an updated version of the original rather than something else entirely? You could easily end up with a client (or another server) creating the same id for something that is already on a different server and then overwriting it by mistake. This is the original reason for server assigned id values.

If there is a server-side option, it probably should be much more careful than just examining resource id values. First, it should explicitly know that it just received a transaction of resources for transfer. I recommend that we create a new Bundle.type code called transfer. At a minimum, the client would change the Bundle.type from Step #1, and now the server has some context. Rather than just match on id values, it can now do something more intelligent — using whatever patient matching algorithms it supports. With a transfer, the server could optionally save the current id values as identifier values when they are persisted. Another option, is the creation of a new $transfer operation, since we’re talking about advanced logic here, beyond simple transaction CRUD operations.

Please share your thoughts and comments below.

About jwalonoski

Jason is a software engineer at the MITRE Corporation. He enjoys fermented beverages, overweight felines, and his family. He dislikes social media.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s