> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ferrum.thalamiq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Batch & Transactions

> Process multiple requests with batch or transaction Bundles.

<Info>
  Spec reference:
  [https://hl7.org/fhir/http.html#transaction](https://hl7.org/fhir/http.html#transaction)
</Info>

Ferrum supports **FHIR Bundles as multi-request envelopes** when you `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

This page documents **Ferrum-specific behavior**, especially the transaction mechanics.

## 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 a `Bundle` to the base endpoint:

```bash theme={null}
curl -X POST "http://localhost:8080/fhir" \
  -H "Content-Type: application/fhir+json" \
  -d '{ "resourceType": "Bundle", "type": "transaction", "entry": [] }'
```

The handler accepts:

* `type: "batch"`
* `type: "transaction"`
* `type: "history"` (replication; not covered here)

<Note>
  In `Bundle.entry.request.url`, Ferrum 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.
</Note>

## Response Detail Level (`Prefer`)

Ferrum supports these response styles for **both batch and transaction**:

* `Prefer: return=minimal` → status/location/etag only
* `Prefer: return=representation` → include the resource bodies in the response bundle
* `Prefer: return=OperationOutcome` → include an `OperationOutcome` in `entry.response.outcome`

## Batch Bundles (`type=batch`)

Batch entries are processed **independently** (FHIR “batch” semantics).

### Independence rules Ferrum enforces

Ferrum pre-validates and flags non-conformant patterns per entry:

* No reference resolution between entries: if a resource contains `Reference.reference` equal to a
  `fullUrl` of a `POST` entry in the same batch, that entry is rejected.
* No change-interdependencies: multiple `PUT`/`PATCH`/`DELETE` entries targeting the same
  `{type}/{id}` are rejected.

<Tip>
  If you need entries to reference each other, use a `transaction` bundle.
</Tip>

### Error shape

Batch requests typically return `HTTP 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**: Ferrum runs the entire bundle inside a single database transaction.

* If every entry succeeds → commit and return `Bundle.type = transaction-response` (HTTP `200`).
* If any entry fails → rollback and return a normal error response (non-2xx) and **no changes are persisted**.

<Note>
  Error messages include entry context like `Transaction entry {index}: ...` to help you pinpoint
  which entry triggered the rollback.
</Note>

### Processing order (important)

Ferrum processes entries in this order (regardless of original order):

1. `DELETE`
2. `POST` (create)
3. `PUT` / `PATCH` (update)
4. Finalize “resolve-as-version-specific” references (see below)
5. `GET` / `HEAD` (read)

This order matches the implementation in `server/src/services/transaction.rs`.

### Constraints Ferrum enforces

* No duplicate `fullUrl` values across entries.
* No identity overlaps for change interactions: you can’t have multiple `DELETE`/`PUT`/`PATCH`
  entries targeting the same `{type}/{id}` inside one transaction (to avoid order-dependent
  outcomes).
* `GET`/`HEAD` inside a transaction is instance-read only; if the URL has no id (looks like a
  type-level search), Ferrum returns an empty `searchset` bundle 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`

Ferrum implements this by building a mapping and rewriting resources **before writing them**:

1. For non-`POST` entries, if `entry.fullUrl` exists and the request URL is an identity
   (`{type}/{id}`), Ferrum maps `fullUrl → "{type}/{id}"`.
2. For `POST` entries, Ferrum **pre-reserves UUID ids** and maps `fullUrl → "{type}/{uuid}"` before
   processing entries, so later entries can safely reference the would-be created resources.
3. Before `POST`/`PUT`/`PATCH`, Ferrum rewrites string values inside the resource JSON:
   * Exact `fullUrl` matches are replaced (fragment-aware: `urn:...#frag` works).
   * Generic string replacement is also applied (e.g., narrative strings), except under canonical
     element paths.

<Note>
  Canonical fields are intentionally excluded from rewriting so canonical URLs don’t accidentally
  get rewritten when they happen to contain a `fullUrl` substring.
</Note>

#### Conditional references inside resources

Ferrum supports **conditional references** (FHIR “search URIs”) inside request resources:

Example:

```xml theme={null}
<subject>
  <reference value="Patient?identifier=http://example.org/fhir/mrn|12345"/>
</subject>
```

Rules (FHIR-style):

* Ferrum searches the target type (`Patient` in 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** with `412` (other entries are unaffected)
* If there is **exactly 1 match**, Ferrum replaces the search URI with a normal reference
  (`Patient/{id}`).

See also: [CRUD Operations → Conditional References](/server/crud-operations#conditional-references-search-uris).

### Conditional operations inside a transaction

Ferrum 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 with `If-None-Match`)
* Conditional patch: `PATCH {type}?{criteria}`
* Conditional delete: `DELETE {type}?{criteria}`

### Concurrency control (`If-Match`, `If-None-Match`)

Ferrum enforces version checks where applicable:

* `If-Match: W/"{versionId}"` is validated for `PUT`, `PATCH`, and `DELETE`.
* Conditional `PUT` supports `If-None-Match: *` semantics (via the conditional update resolver).

### PATCH in transactions (JSON Patch via `Binary`)

For transaction `PATCH`, Ferrum 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

Security/safety note: after applying the patch, Ferrum removes `resource.text` (narrative) because it
may no longer match the updated data.

Example entry shape (data omitted for brevity):

```json theme={null}
{
  "request": { "method": "PATCH", "url": "Patient/123" },
  "resource": {
    "resourceType": "Binary",
    "contentType": "application/json-patch+json",
    "data": "BASE64_ENCODED_JSON_PATCH_BYTES"
  }
}
```

### Version-specific references (`resolve-as-version-specific`)

Ferrum 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}`), Ferrum 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`

```json theme={null}
{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:patient-1",
      "request": { "method": "POST", "url": "Patient" },
      "resource": { "resourceType": "Patient", "name": [{ "family": "Doe" }] }
    },
    {
      "request": { "method": "POST", "url": "Observation" },
      "resource": {
        "resourceType": "Observation",
        "status": "final",
        "subject": { "reference": "urn:uuid:patient-1" }
      }
    }
  ]
}
```

Ferrum will rewrite `subject.reference` to `Patient/{uuid}` before inserting the Observation.

### Transaction: conditional create (`ifNoneExist`)

```json theme={null}
{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:patient-1",
      "request": {
        "method": "POST",
        "url": "Patient",
        "ifNoneExist": "identifier=http://acme.example/mrn|123"
      },
      "resource": {
        "resourceType": "Patient",
        "identifier": [{ "system": "http://acme.example/mrn", "value": "123" }]
      }
    }
  ]
}
```

If a match exists, the entry returns `200 OK` and `fullUrl` mapping points at the existing resource.

## Related docs

* API reference: [Transaction](/api-reference/batch/transaction), [Batch](/api-reference/batch/batch)
* Learn FHIR: [Bundles](/learn-fhir/bundles), [Batch & Transaction](/learn-fhir/batch-and-transaction)
