FHIR includes a mechanism that can be used by a client to send multiple interactions to a server for processing. This mechanism uses the FHIR Bundle Resource as the transport, with a collection of one or more interactions grouped inside the Bundle.
FHIR Transactions are sent to the server using an HTTP POST to the base URL of the server. There are two modes of processing, as determined by the Bundle.type
value:
According to the standard, any HTTP REST operation is a candidate to be included in a transaction. In practice, transactions are most commonly used to write data to a server.
A transaction Bundle has a few notable parts:
Bundle.type
value specifies the processing mode (transaction or batch).Bundle.entry
array contains a single interaction, and is the equivalent to a single HTTP REST interaction.Bundle.entry.request
.Bundle.entry.resource
.Bundle.entry.fullUrl
. This element is explained in more detail in the sections below.The following example shows a simple FHIR create using a FHIR transaction. In this simple example the fullUrl is a randomly generated UUID. It may appear in server log messages but has no meaning or use otherwise and is not stored anywhere else.
{ "resourceType": "Bundle", | |
Bundle Type | "type": "transaction", |
"entry": [ { | |
Entry Full URL | "fullUrl": "urn:uuid:b9c464c7-f29c-42f9-9f95-9c6e1f918abc", |
"resource": { | |
Resource Body | "resourceType": "Patient", "identifier": [ { "system": "http://acme.org/mrns", "value": "013872" } ], "name": [ { "family": "Simpson", "given": [ "Homer" ] } ] |
}, "request": { | |
Interaction HTTP Verb (POST, GET, PUT, etc) | "method": "POST", |
Interaction REST URL (relative to server base URL) | "url": "Patient" |
} } ] } |
For a FHIR server with a base URL of http://localhost:8000, the Bundle above would be POST-ed to the base URL with no additional path.
A common use for FHIR transactions is to store a collection of related resources to a server. For example, if you have a collection of Observation resources with the same subject, you could place them inside a single FHIR transaction and send them together to a server. In fact, there is even no requirement for these resources to have the same subject.
Using a FHIR transaction to group resources being written has several advantages, with performance being the most important one. This includes savings from avoiding multiple HTTP round trips, but also includes efficiencies that the server can gain while processing multiple writes at the same time. For example, ID and reference lookups can be performed once and reused each time they appear in the Bundle.
If you have large amounts of data that you need to load into a server, using transactions can be a much more efficient way to accomplish this when compared to executing individual operations. The specifics will always depend on your exact setup and data, but in tests we have often found that a single transaction Bundle with 1000 resources to create will process up to 50x faster than executing these as individual writes.
The simplest way to transmit related resources is to use client-assigned IDs. When using this form, the server is instructed to use the IDs supplied in the requests. Any references between resources simply use these client-assigned IDs. Resources may also have references to other resources which already exist on the server.
Using client-assigned IDs in this way can have advantages. Uploading the transaction bundle multiple times should have no impact, since the second time will result in no-op updates. Resources with predictable IDs can be helpful for traceability as well. However, using client-assigned IDs is not always practical, so other mechanisms will be shown in the sections below.
The following example shows a transaction bundle that performs an upsert with client-assigned ID on 3 resources: a patient, and 2 observations for this patient.
{ "resourceType": "Bundle", "type": "transaction", "entry": [ { | |
First Entry - Create a Patient | "fullUrl": "Patient/PTA", "resource": { "resourceType": "Patient", "id": "PTA", "identifier": [ { "system": "http://acme.org/mrns", "value": "013872" } ], "name": [ { "family": "Simpson", "given": [ "Homer" ] } ] }, "request": { "method": "PUT", "url": "Patient/PTA" } |
}, { | |
Second Entry - Create an Observation | "fullUrl": "Observation/OB1", "resource": { "resourceType": "Observation", "id": "OB1", "status": "final", "code": { "coding": [ { "system": "http://loinc", "code": "29463-7", "display": "Body Weight" } ] }, "subject": { "reference": "Patient/PTA" }, "effectiveDateTime": "2022-02-23", "valueQuantity": { "value": 67.1, "unit": "kg", "system": "http://unitsofmeasure.org", "code": "kg" } }, "request": { "method": "PUT", "url": "Observation/OB1" } |
}, { | |
Third Entry - Create another Observation | "fullUrl": "Observation/OB2", "resource": { "resourceType": "Observation", "id": "OB2", "status": "final", "code": { "coding": [ { "system": "http://loinc", "code": "29463-7", "display": "Body Weight" } ] }, "subject": { "reference": "Patient/PTA" }, "effectiveDateTime": "2019-12-29", "valueQuantity": { "value": 72.4, "unit": "kg", "system": "http://unitsofmeasure.org", "code": "kg" } }, "request": { "method": "PUT", "url": "Observation/OB2" } |
} ] } |
Creating references between resources is easy if you know the ID of the reference target (such as in the example above with client-assigned IDs) but it is also possible to have references between resources in a single bundle even if you don't yet know the target resource ID. For example, if an entry in your Bundle is using a normal FHIR create (i.e. HTTP POST) then it will assign an ID to the resource and your client will not know this ID until after the transaction has been processed.
FHIR solves this issue by using a feature called Placeholder IDs, which are UUIDs generated by the client to associate with each resource. These UUIDs are temporary and should be randomly generated. They serve only to link resources in the Bundle together, and are thrown away by the server once it has determined the actual resource IDs.
The following example shows a simple transaction which creates two resources: a Patient, and an Observation referring to this patient.
{ "resourceType": "Bundle", "type": "transaction", "entry": [ { | |
Placeholder ID for Patient resource | "fullUrl": "urn:uuid:e16eac01-a5ee-4904-b1c8-f4bd56e338d5", |
"resource": { "resourceType": "Patient", "identifier": [ { "system": "http://acme.org/mrns", "value": "013872" } ], "name": [ { "family": "Simpson", "given": [ "Homer" ] } ] }, "request": { "method": "POST", "url": "Patient" } }, { "fullUrl": "urn:uuid:499733fe-7ced-4d15-81ce-8a433a1fb71e", "resource": { "resourceType": "Observation", "status": "final", "code": { "coding": [ { "system": "http://loinc", "code": "29463-7", "display": "Body Weight" } ] }, "subject": { | |
Reference to Patient placeholder ID. This will be automatically replaced with the real resource ID by the server. | "reference": "urn:uuid:e16eac01-a5ee-4904-b1c8-f4bd56e338d5" |
}, "effectiveDateTime": "2022-02-23", "valueQuantity": { "value": 67.1, "unit": "kg", "system": "http://unitsofmeasure.org", "code": "kg" } }, "request": { "method": "POST", "url": "Observation" } } ] } |
The FHIR Conditional Create mechanism allows the client to specify a FHIR search URL alongside a resource to create. When processing the create, the server will first check if any resource already exists matching the given search. If no resources match, the create proceeds. If a resource does match, no resource is created.
In standard REST, conditional creates place the search URL in the In-None-Exist
request header. In a transaction, this URL goes in Bundle.entry.request.ifNoneExist
. Like a normal create, we use the HTTP method/verb of POST
.
An example is shown below.
{ "resourceType": "Bundle", "type": "transaction", "entry": [ { | |
Placeholder ID for Patient | "fullUrl": "urn:uuid:95dbbf93-5829-46ba-9021-2545a1da3aa5", |
"resource": { "resourceType": "Patient", "identifier": [ { "system": "http://acme.org/mrns", "value": "013872" } ], "name": [ { "family": "Simpson", "given": [ "Homer" ] } ] }, "request": { "method": "POST", "url": "Patient", | |
Search URL for Conditional Create | "ifNoneExist": "Patient?identifier=http://acme.org/mrns|013872" |
} }, { "fullUrl": "urn:uuid:124ff3c8-f251-4bd9-8c44-cc6568180eae", "resource": { "resourceType": "Observation", "identifier": [ { "system": "http://acme.org/obs", "value": "46252" } ], "status": "final", "code": { "coding": [ { "system": "http://loinc", "code": "29463-7", "display": "Body Weight" } ] }, "subject": { | |
Reference to Patient Placeholder ID | "reference": "urn:uuid:95dbbf93-5829-46ba-9021-2545a1da3aa5" |
}, "effectiveDateTime": "2022-02-23", "valueQuantity": { "value": 67.1, "unit": "kg", "system": "http://unitsofmeasure.org", "code": "kg" } }, "request": { "method": "POST", "url": "Observation", | |
Search URL for Conditional Create | "ifNoneExist": "Observation?identifier=http://acme.org/obs|46252" |
} } ] } |
Note that by default, if you attempt on different partitions (ex Partition-A and Partition-B) to conditionally create two resources with the same identifier (ex: system:http://acme.org/mrns value:000070003817) the operation will fail due to a primary key violation error. If you wish to support duplicate identifiers across partitions, please enable this key, which is disabled by default: property-allow-conditional-creates-with-duplicate-resource-identifiers-across-partitions.enabled
The FHIR Conditional Update mechanism allows a resource to be transmitted to the server for updating, but instead of supplying an ID to update, the client supplies a search URL similar to the URL used for Conditional Create above.
Before performing the update, the server first performs the search. If no resources match the search, a new resource is created. If a resource already matches the search, it is updated using the contents in Bundle.entry.resource
.
In this case, the method/verb will be PUT
and the search URL is specified in Bundle.request.url
.
An example is shown below.
{ "resourceType": "Bundle", "type": "transaction", "entry": [ { | |
Placeholder ID for Patient | "fullUrl": "urn:uuid:95dbbf93-5829-46ba-9021-2545a1da3aa5", |
"resource": { "resourceType": "Patient", "identifier": [ { "system": "http://acme.org/mrns", "value": "013872" } ], "name": [ { "family": "Simpson", "given": [ "Homer" ] } ] }, "request": { "method": "PUT", | |
Search URL for Conditional Update | "url": "Patient?identifier=http://acme.org/mrns|013872" |
} }, { "fullUrl": "urn:uuid:124ff3c8-f251-4bd9-8c44-cc6568180eae", "resource": { "resourceType": "Observation", "identifier": [ { "system": "http://acme.org/obs", "value": "46252" } ], "status": "final", "code": { "coding": [ { "system": "http://loinc", "code": "29463-7", "display": "Body Weight" } ] }, "subject": { | |
Reference to Patient Placeholder ID | "reference": "urn:uuid:95dbbf93-5829-46ba-9021-2545a1da3aa5" |
}, "effectiveDateTime": "2022-02-23", "valueQuantity": { "value": 67.1, "unit": "kg", "system": "http://unitsofmeasure.org", "code": "kg" } }, "request": { "method": "PUT", | |
Search URL for Conditional Update | "url": "Observation?identifier=http://acme.org/obs|46252" |
} } ] } |
The various create and update and conditional variant interactions can be mixed and matched in a single transaction as well. A common scenario is to want to create new resources associated with a Patient, and to only create the Patient if it does not already exist but leave it in place and not modify it if it does already exist.
This can be accomplished by using a conditional create on the Patient resource and a normal create on the referring resource (e.g. an Observation), as shown in the example below. You could also use conditional updates or upsert with client-assigned ID on the Observation, depending on the semantics you want.
{ "resourceType": "Bundle", "type": "transaction", "entry": [ { "fullUrl": "urn:uuid:11679713-c5f0-42d6-a702-7eb08a29b249", "resource": { "resourceType": "Patient", "identifier": [ { "system": "http://acme.org/mrns", "value": "013872" } ], "name": [ { "family": "Simpson", "given": [ "Homer" ] } ] }, "request": { | |
Conditionally create the Patient | "method": "POST", "url": "Patient", "ifNoneExist": "Patient?identifier=http://acme.org/mrns|013872" |
} }, { "fullUrl": "urn:uuid:666a2660-1b1e-49ac-bc8f-dd7e4908421f", "resource": { "resourceType": "Observation", "identifier": [ { "system": "http://acme.org/obs", "value": "46252" } ], "status": "final", "code": { "coding": [ { "system": "http://loinc", "code": "29463-7", "display": "Body Weight" } ] }, "subject": { "reference": "urn:uuid:11679713-c5f0-42d6-a702-7eb08a29b249" }, "effectiveDateTime": "2022-02-23", "valueQuantity": { "value": 67.1, "unit": "kg", "system": "http://unitsofmeasure.org", "code": "kg" } }, | |
Normal create for the Observation | "request": { "method": "POST", "url": "Observation" } |
} ] } |
FHIR logical deletes can be performed as a part of a transaction as well. In a trasnaction, deletes will be applied as a group meaning that if one fails, all will be rolled back.
The following example shows deletes in a FHIR transaction.
{ "resourceType": "Bundle", "type": "transaction", "entry": [ { | |
A delete entry. No resource payload is needed or allowed. | "request": { "method": "DELETE", "url": "Patient/A0" } |
}, { | |
A second delete entry. | "request": { "method": "DELETE", "url": "Patient/A1" |
} } ] } |
The FHIR Patch operation can also be performed in a transaction. When using the FHIR Patch mechanism for patching, the FHIR Patch document is placed in Bundle.entry.resource
. In the case of JSON Patch or XML Patch, the contents are placed in a Binary resource and then placed into Bundle.entry.resource
. In all cases, the HTTP verb/method is PATCH
.
The following example shows a simple FHIR Patch in a transaction.
{ "resourceType": "Bundle", "type": "transaction", "entry": [ { "resource": { | |
The FHIR Patch document | "resourceType": "Parameters", "parameter": [ { "name": "operation", "part": [ { "name": "type", "valueCode": "replace" }, { "name": "path", "valueString": "Patient.identifier" }, { "name": "value", "valueIdentifier": { "system": "http://new-system", "value": "0001" } } ] } ] |
}, "request": { "method": "PATCH", | |
The identity of the resource to patch | "url": "Patient/123" |
} } ] } |
An interesting use case involves wanting to create a resource if it does not already exist, but to make only a specific change if it does.
The example below shows such a transaction.
{ "resourceType": "Bundle", "type": "transaction", "entry": [ { | |
The resource to create if it does not already exist | "resource": { "resourceType": "Patient", "identifier": [ { "system": "http://system", "value": "value" } ], "active": false }, |
"request": { "method": "POST", "url": "Patient", | |
Conditional create URL | "ifNoneExist": "Patient?identifier=http://system|value" |
} }, { | |
FHIR Patch to perform on the resource | "resource": { "resourceType": "Parameters", "parameter": [ { "name": "operation", "part": [ { "name": "type", "valueCode": "replace" }, { "name": "path", "valueCode": "Patient.active" }, { "name": "value", "valueBoolean": true } ] } ] }, |
"request": { "method": "PATCH", | |
Conditional patch URL | "url": "Patient?identifier=http://system|value" |
} } ] } |