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.
FHIR transactions are processed as a single atomic database transaction. This means that if any individual write operations within the transaction fail, the entire transaction will be rolled back.
FHIR transactions can be used to create new resources and update existing resources, including the ability to create resources with references to each other. See Placeholder IDs for information on how to do this. FHIR transactions are generally faster for writing data, and can be significantly faster depending on the specific data and the size of the transaction.
The transaction request 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" |
} } ] } |
A FHIR transaction request is a Bundle containing one or more entries for processing. The server will respond with a FHIR transaction containing entries corresponding to the entries in the request bundle. The first entry in the response Bundle corresponds to the first entry in the request Bundle, the second corresponds to the second, etc.
The following example shows a Transaction response for a Transaction containing two entries: The first resource was created using a client-assigned ID, and the second already existed on the server and was unchanged by the transaction (because the submitted resource body matched the body what was already stored).
Note the following details:
"status"
element corresponds to the HTTP Status code which would be returned if the entry had been submitted as an individual raw HTTP transaction."code"
element contains a code indicating exactly how the server processed the entry. A complete list of possible codes can be found at Storage Outcome Status Codes. An additional code may also be present if the entry resulted in the creation of a Placeholder Reference Target.{ "resourceType": "Bundle", "type": "transaction-response", "entry": [ { "response": { | |
The first entry resulted in an HTTP 201 Created with ID OBS123 | "status": "201 Created", "location": "Observation/OBS123/_history/1", |
"etag": "1", "lastModified": "2025-01-08T17:17:19.583-05:00", "outcome": { "resourceType": "OperationOutcome", "issue": [ { "severity": "information", "code": "informational", "details": { "coding": [ { | |
This was an "update as create", meaning a create with a client-assigned ID | "system": "https://hapifhir.io/fhir/CodeSystem/hapi-fhir-storage-response-code", "code": "SUCCESSFUL_UPDATE_AS_CREATE", "display": "Update as create succeeded." |
} ] }, "diagnostics": "Successfully created resource \"Observation/OBS123/_history/1\" using update as create (ie. create with client assigned ID). Took 71ms." } ] } } }, { "response": { | |
The second entry was also successful, but did not result in a creation | "status": "200 OK", "location": "Patient/A/_history/1", |
"etag": "1", "outcome": { "resourceType": "OperationOutcome", "issue": [ { "severity": "information", "code": "informational", "details": { "coding": [ { | |
This update was a no-op as no change was actually detected. | "system": "https://hapifhir.io/fhir/CodeSystem/hapi-fhir-storage-response-code", "code": "SUCCESSFUL_UPDATE_NO_CHANGE", "display": "Update succeeded: No changes were detected so no action was taken." |
} ] }, "diagnostics": "Successfully updated resource \"Patient/A/_history/1\" with no changes detected." } ] } } } ] } |
The FHIR Batch operation is similar to the FHIR Transaction operation in that it is a collection of individual operations. However, it is not executed as a single database transaction but instead each entry is executed independently. This can be advantageous if you are loading data and do not want the failure to process one resource to prevent the processing of another.
Using a batch can still be faster than submitting individual discrete HTTP transactions, but it is typically less performant than using a FHIR Transaction, especially for large amounts of data.
{ "resourceType": "Bundle", | |
Batch bundle type | "type": "batch", |
"entry": [ { | |
First entry: Create a Patient | "resource": { "resourceType": "Patient", "identifier": [ { "system": "http://example.com/identifiers", "value": "123" } ] }, "request": { "method": "POST", "url": "Patient" } |
}, { | |
Second entry: Create a second Patient | "resource": { "resourceType": "Patient", "identifier": [ { "system": "http://example.com/identifiers", "value": "456" } ] }, "request": { "method": "POST", "url": "Patient" } |
} ] } |
The response object for a FHIR Batch operation looks similar to the response for a FHIR Transaction operation, but it can have a mix of successful responses and failures.
Consider the following request, which has a valid entry as well as an invalid entry which is expected to fail.
{ "resourceType": "Bundle", "type": "batch", "entry": [ { "resource": { "resourceType": "Patient", "identifier": [ { "system": "urn:system", "value": "FOO" } ] }, "request": { "method": "POST", "url": "Patient" } }, { | |
This entry will fail, as the request.url value is invalid for the resource type | "resource": { "resourceType": "Patient", "identifier": [ { "system": "urn:system", "value": "BAR" } ] }, "request": { "method": "PUT", "url": "Observation/123" } |
} ] } |
The following example shows a response generated when processing this Batch.
{ "resourceType": "Bundle", "type": "batch-response", "entry": [ { | |
This entry succeeded | "response": { "status": "201 Created", "location": "Patient/1/_history/1", "etag": "1", "lastModified": "2025-01-08T17:11:32.839-05:00", "outcome": { "resourceType": "OperationOutcome", "issue": [ { "severity": "information", "code": "informational", "details": { "coding": [ { "system": "https://hapifhir.io/fhir/CodeSystem/hapi-fhir-storage-response-code", "code": "SUCCESSFUL_CREATE", "display": "Create succeeded." } ] }, "diagnostics": "Successfully created resource \"Patient/1/_history/1\". Took 42ms." } ] } } |
}, { | |
This entry failed | "response": { "status": "400 Bad Request", "outcome": { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "exception", "diagnostics": "HAPI-2616: Can not process entity with ID[Observation/123], this is not the correct resource type for this resource" } ] } } |
} ] } |