Spec reference:
https://hl7.org/fhir/http.html#transaction
POST a
Bundle to the server base endpoint (POST /fhir).
Bundle.type = batch: entries are independent (partial success is expected)Bundle.type = transaction: atomic (all-or-nothing), with intra-transaction reference rewriting
Quick Comparison
| Aspect | batch | transaction |
|---|---|---|
| Atomic | ❌ No | ✅ Yes |
Inter-entry references via fullUrl | ❌ Rejected (non-conformant) | ✅ Supported (rewritten) |
Conditional references (Patient?identifier=...) | ✅ Supported (must resolve to exactly one match) | ✅ Supported (must resolve to exactly one match) |
| Overall HTTP status | 200 (with per-entry statuses) | 200 on success; non-2xx on any failure |
| Processing order | DELETE → POST → PUT/PATCH → GET/HEAD | DELETE → POST → PUT/PATCH → (finalize refs) → GET/HEAD |
| Conditional ops (If-None-Exist / criteria URLs) | ✅ Supported | ✅ Supported (resolved inside the DB transaction) |
Endpoint and Request Shape
Send aBundle to the base endpoint:
type: "batch"type: "transaction"type: "history"(replication; not covered here)
In
Bundle.entry.request.url, TLQ FHIR accepts both relative and absolute URLs (it strips scheme/host
before parsing). Example: Patient/123 and https://example.com/fhir/Patient/123 are treated
the same.Response Detail Level (Prefer)
TLQ FHIR supports these response styles for both batch and transaction:
Prefer: return=minimal→ status/location/etag onlyPrefer: return=representation→ include the resource bodies in the response bundlePrefer: return=OperationOutcome→ include anOperationOutcomeinentry.response.outcome
Batch Bundles (type=batch)
Batch entries are processed independently (FHIR “batch” semantics).
Independence rules TLQ FHIR enforces
TLQ FHIR pre-validates and flags non-conformant patterns per entry:- No reference resolution between entries: if a resource contains
Reference.referenceequal to afullUrlof aPOSTentry in the same batch, that entry is rejected. - No change-interdependencies: multiple
PUT/PATCH/DELETEentries targeting the same{type}/{id}are rejected.
Error shape
Batch requests typically returnHTTP 200 with a Bundle.type = batch-response. Errors are
represented per entry via entry.response.status and an OperationOutcome.
Transaction Bundles (type=transaction)
Transactions are atomic: TLQ FHIR runs the entire bundle inside a single database transaction.
- If every entry succeeds → commit and return
Bundle.type = transaction-response(HTTP200). - If any entry fails → rollback and return a normal error response (non-2xx) and no changes are persisted.
Error messages include entry context like
Transaction entry {index}: ... to help you pinpoint
which entry triggered the rollback.Processing order (important)
TLQ FHIR processes entries in this order (regardless of original order):DELETEPOST(create)PUT/PATCH(update)- Finalize “resolve-as-version-specific” references (see below)
GET/HEAD(read)
server/src/services/transaction.rs.
Constraints TLQ FHIR enforces
- No duplicate
fullUrlvalues across entries. - No identity overlaps for change interactions: you can’t have multiple
DELETE/PUT/PATCHentries targeting the same{type}/{id}inside one transaction (to avoid order-dependent outcomes). GET/HEADinside a transaction is instance-read only; if the URL has no id (looks like a type-level search), TLQ FHIR returns an emptysearchsetbundle entry.
fullUrl mapping and intra-transaction reference rewriting
Transactions support the common FHIR pattern:
- Create a resource in one entry (
POST) - Reference it from another entry using the first entry’s
fullUrl
- For non-
POSTentries, ifentry.fullUrlexists and the request URL is an identity ({type}/{id}), TLQ FHIR mapsfullUrl → "{type}/{id}". - For
POSTentries, TLQ FHIR pre-reserves UUID ids and mapsfullUrl → "{type}/{uuid}"before processing entries, so later entries can safely reference the would-be created resources. - Before
POST/PUT/PATCH, TLQ FHIR rewrites string values inside the resource JSON:- Exact
fullUrlmatches are replaced (fragment-aware:urn:...#fragworks). - Generic string replacement is also applied (e.g., narrative strings), except under canonical element paths.
- Exact
Canonical fields are intentionally excluded from rewriting so canonical URLs don’t accidentally
get rewritten when they happen to contain a
fullUrl substring.Conditional references inside resources
TLQ FHIR supports conditional references (FHIR “search URIs”) inside request resources: Example:- TLQ FHIR searches the target type (
Patientin the example) using the query string parameters. - If there are 0 matches or >1 match, the interaction fails:
transaction: the transaction fails (and everything rolls back)batch: the entry fails with412(other entries are unaffected)
- If there is exactly 1 match, TLQ FHIR replaces the search URI with a normal reference
(
Patient/{id}).
Conditional operations inside a transaction
TLQ FHIR supports the common conditional patterns and resolves them within the transaction, meaning: searches see the effects of earlier writes in the same bundle. Supported patterns:- Conditional create:
POST+entry.request.ifNoneExist - Conditional update:
PUT {type}?{criteria}(optionally withIf-None-Match) - Conditional patch:
PATCH {type}?{criteria} - Conditional delete:
DELETE {type}?{criteria}
Concurrency control (If-Match, If-None-Match)
TLQ FHIR enforces version checks where applicable:
If-Match: W/"{versionId}"is validated forPUT,PATCH, andDELETE.- Conditional
PUTsupportsIf-None-Match: *semantics (via the conditional update resolver).
PATCH in transactions (JSON Patch via Binary)
For transaction PATCH, TLQ FHIR expects the entry’s resource to be a Binary containing a JSON Patch
document:
Binary.contentType = "application/json-patch+json"Binary.data= base64-encoded JSON Patch bytes
resource.text (narrative) because it
may no longer match the updated data.
Example entry shape (data omitted for brevity):
Version-specific references (resolve-as-version-specific)
TLQ FHIR implements the FHIR extension:
http://hl7.org/fhir/StructureDefinition/resolve-as-version-specific
If a Reference element has this extension and contains a versionless local reference
({type}/{id}), TLQ FHIR will (after all writes) rewrite it to a version-specific reference:
{type}/{id}/_history/{versionId}
…but only when the referenced target was written in the same transaction. The extension is removed
as part of resolution.
Examples
Transaction: create + reference via fullUrl
subject.reference to Patient/{uuid} before inserting the Observation.
Transaction: conditional create (ifNoneExist)
200 OK and fullUrl mapping points at the existing resource.
Related docs
- API reference: Transaction, Batch
- Learn FHIR: Bundles, Batch & Transaction