Every FHIR Resource Is Small Because References Do the Work
FHIR resources are deliberately narrow. An Observation does not embed the patient it belongs to; it points at one. A Condition does not embed the clinician who asserted it; it points at one. An Encounter does not embed the organisation that provided the service; it points at one. This is what keeps individual resources small, cacheable, and independently updatable — and it is what makes references the load-bearing element of the entire standard.
It also means that a resource in isolation is frequently uninterpretable. An Observation with "subject": {"reference": "Patient/034AB16"} tells you a measurement was taken, but nothing about whom, until that reference resolves. When it does not resolve — because the target was never included, because the reference is a placeholder from someone else's transaction, because a system rewrote ids on ingestion — you get the single most common class of FHIR integration failure: data that is structurally valid and semantically orphaned.
This guide covers the forms a reference can take, how each is meant to resolve, the special rules that apply inside Bundles and transactions, and what to do when references do not resolve. Every example can be pasted into the FHIR Bundle Explorer, which builds a reference map locally in your browser.
The Reference Data Type
Wherever one resource points at another, FHIR uses the Reference data type. It is not just a string — it has four elements, and understanding all four prevents a lot of confusion:
reference— the literal reference: a URL that identifies the target. This is the field everyone means when they say "reference."type— the resource type of the target, expressed as a URI relative tohttp://hl7.org/fhir/StructureDefinition/, so in practice a plain code like"Patient". When supplied it must agree with the type determined by resolving the reference.identifier— a logical reference: a business identifier for the target rather than an address.display— a short human-readable description of the target, for rendering when you cannot or do not want to resolve it.
A Reference may carry a literal reference, a logical one, both, or neither. The R4 references page is explicit that when both are present, the literal reference is preferred, and applications are permitted but not required to check that the two agree.
Literal References: The Three Forms
The R4 specification states that Reference.reference contains a URL that is one of exactly three things: an absolute URL, a relative URL, or an internal fragment reference. Everything else in this article follows from those three cases.
Relative References
The form you will see most: "reference": "Patient/034AB16". It is a resource type and a logical id, and it is resolved relative to the service base URL. On a server whose base is https://hospital.example.org/fhir/, that reference means https://hospital.example.org/fhir/Patient/034AB16.
Relative references are compact and portable within an ecosystem — the same payload works against dev, staging, and production because the base changes and the references do not. Their weakness is exactly that portability: a relative reference is meaningless without knowing the base. Copy an Observation out of one server's response and into another system, and Patient/034AB16 now silently points at whatever patient happens to occupy that id on the new server. This is not a hypothetical: id collisions across systems are the reason many organisations reassign ids at the boundary, which in turn is why references arrive broken.
Absolute References
The self-describing form: "reference": "https://hospital.example.org/fhir/Patient/034AB16". It says exactly where the target lives and survives being copied anywhere. The specification notes that absolute URLs provide a stable, scalable approach suited to a cloud or web context, while relative references suit trading within a closed ecosystem.
A few properties are worth remembering. Absolute references do not have to point at a FHIR RESTful server, though the spec calls that the preferred approach. URLs are always case-sensitive — Patient/ABC and Patient/abc are different resources, and a case-normalising middleware layer will break links. And a reference can be version-specific by including a history segment, as in http://example.org/fhir/Observation/1x2/_history/2, which is usually seen in provenance and audit contexts where you must point at exactly the version that was seen, not the current one.
urn:uuid References
The third form has no external existence at all. Inside a Bundle you are constructing, you may not yet know what ids the server will assign, so you invent identities: each entry's fullUrl is set to a value like urn:uuid:c72aa430-2ddc-456e-7a09-dea8264671d8, and the other entries reference that URN. The R4 references page notes that in a transaction, reference URLs may contain logical URIs such as OIDs or UUIDs that resolve within the transaction, and that when processing the transaction the server replaces the logical URL with the correct literal URL at completion.
The crucial property of a urn:uuid: reference is that it is only meaningful inside its own Bundle. Extract one entry from a transaction Bundle and store it on its own, and its references now point at nothing anywhere in the universe. This is a startlingly frequent failure mode in ETL pipelines that split Bundles into per-resource records without a resolution step.
Contained Resources: References That Point Inward
Sometimes the target of a reference cannot exist independently. The R4 specification gives a precise example: an interface engine building a Condition from an HL7 v2 message knows only the primary surgeon's first and last name, taken from the REL segment. Without a controlled practitioner directory, that is not enough to create an identified Practitioner — more than one practitioner may share a name — and any id you invented would be meaningless outside this one Condition.
For these cases FHIR allows the resource to be placed inline in the contained element of the parent, and referenced with a fragment: "reference": "#p1", pointing at the contained resource whose id is p1. A reference of just "#" points at the container itself, which is how a contained Provenance targets its parent.
The spec attaches firm rules to containment, and they are the ones people break:
- Contained resources SHALL NOT contain further contained resources. Containment is one level deep, always.
- References to contained resources are never resolved outside the container resource. Resolution stops at
Bundle.entry.resourceandParameters.parameter.resource. - Contained resources SHALL NOT carry
meta.versionId,meta.lastUpdated, ormeta.security. They MAY carrymeta.tag. - A resource SHALL only be contained if something in the container references it, or if the contained resource references the container.
- Contained resources do not inherit context from the parent — a contained resource with its own
subjectelement makes no claim that it shares the parent's subject. - The FHIR version of a contained resource is always the same as its container.
The specification is also blunt about when not to contain: this SHOULD NOT be done when the content can be identified properly, because once identity is lost it is extremely difficult to restore. Containment is a fallback for genuinely unidentifiable data, not a convenience for avoiding a second POST.

Resolving References Inside a Bundle
The R4 Bundle page defines an explicit algorithm for reference resolution within a Bundle, and it opens with the rule that matters most: applications reading a Bundle should always look for a resource by its identity in the Bundle first, before trying to access it externally. The Bundle is the local universe; only if a reference misses locally does the outside world come into play.
The procedure, paraphrased from the specification, runs as follows. If the reference is not already absolute, convert it: when it has the form [type]/[id] and the fullUrl of the entry containing the referring resource is a RESTful URL, extract the root from that fullUrl and append the reference to it, then resolve the result within the Bundle as an absolute URL. If the reference is already absolute, look for an entry whose fullUrl matches it; if no entry matches and the URI is a resolvable URL, it may be fetched directly. If the reference is version-specific, strip the version before matching fullUrl and then match the version using Resource.meta.versionId. The rules for resolving references inside contained resources are the same as for the containing resource. And if multiple entries match, it is ambiguous which is correct; applications may return an error or take whatever action they deem appropriate.
Two consequences follow. First, the base URL used for a relative reference comes from the referring entry's own fullUrl, not from some global setting — a Bundle assembled from two servers can legitimately contain relative references resolving against two different bases. Second, when the referring entry has no fullUrl, or it is not a RESTful URL, a relative reference cannot be converted, and the spec says plainly that the reference then has no defined meaning within this specification.
Reference Rewriting in a Transaction
Transactions add a further mechanism. When a server processes a transaction and assigns a new id to a resource submitted with a POST, it SHALL also update any references to that resource in the same Bundle as they are processed. References to resources that are not part of the Bundle are left untouched.
The scope of that rewriting is wider than most people expect. The specification says servers SHALL replace all matching links in the Bundle wherever they occur: in resource ids, in resource references, in elements of type uri, url, oid, and uuid, and even inside href and src attributes in narrative. Elements of type canonical are explicitly not replaced.
There is one further trick available only inside a transaction: conditional references. Instead of a placeholder, a reference may be a search URI describing how to find the target — for example "reference": "Patient?identifier=12345". The spec notes this arises commonly when building transactions from v2 messages, where the client knows an MRN but not a FHIR logical id. When processing, the server checks all references for search URIs, runs the search, and if there is exactly one match replaces the search URI with a reference to it. If there are zero matches or more than one, the transaction fails. That all-or-nothing behaviour is a feature — an ambiguous patient match should stop a write, not guess.
Note also that when processing a POST, the server treats the fullUrl as the source system's id and ignores it, generating its own. For updates it attempts to map the supplied fullUrl to a local URL, and if it has no mapping it ignores the base and assumes its own. And a server MAY simply assign new ids to everything regardless of any claimed logical id, because honouring client ids is only safe in controlled circumstances.
Why Unresolved References Break Integrations
An unresolved reference is rarely a hard crash. It is worse than that: it is silent data loss that surfaces later, in an analysis or a clinical view, as absence rather than error. The common causes are worth listing because each has a different fix.
- The target was simply not included. A search returned Observations without
_include=Observation:subject, so every subject reference points outside the Bundle. Nothing is wrong with the data; the extract is incomplete. - Placeholders escaped their Bundle. A transaction was split into individual resources without resolving
urn:uuid:references first. The references are now permanently meaningless. - Ids were reassigned on ingestion. The receiving system generated its own ids — as the spec permits — but a downstream copy of the data kept the original references. Every link now points at the wrong resource or none.
- Base URL mismatch. Relative references were resolved against the wrong base because the entries carried no
fullUrl, or a pipeline flattened Bundles from several sources into one file. - Case or trailing-slash drift. URLs are case-sensitive, and a normalising proxy or a hand-edited fixture can silently break otherwise perfect links.
- Contained references treated as global. A
#p1reference means something only inside its container; extracted, it resolves to nothing.
The remedy is the same in every case: measure it. Before a Bundle enters a pipeline, count how many references resolve locally and inspect the ones that do not, then decide deliberately whether each is expected — a legitimate pointer to a resource that lives on a server — or a defect.
Mapping References Without Sending Data Anywhere
The FHIR Bundle Explorer was built around this problem. It indexes every reference string in whatever you paste — a Bundle, a bare resource, NDJSON from a bulk export, or a JSON array — and resolves each one against the resources present, matching on ResourceType/id, on bare id, on the entry's fullUrl, and on the trailing Type/id segment of that fullUrl, which is how entries inside the same Bundle usually address each other. You get outgoing references per resource, incoming references per resource, and an explicit list of everything that did not resolve.
Two design choices are deliberate and worth stating plainly. Unresolved references are shown, never dropped — the whole point is to see the holes, so silently discarding a broken edge would defeat the exercise. And resolution is local only, never over the network: a Bundle is patient data, and quietly fetching a referenced resource from an external server would leak it. Everything runs in your browser; nothing is uploaded.
It is an exploration tool rather than a conformance checker — it will not tell you that a profile required an element you left out. For that, run the same resource through the FHIR Resource Validator. And if you are still working out what kind of Bundle you are holding and why its fullUrl values look the way they do, start with What Is a FHIR Bundle?. If your references arrived in NDJSON from a population-scale extract rather than a single Bundle, FHIR Bulk Data Export Explained covers why that format resolves differently.
Conclusion
A FHIR reference is one of three things: an absolute URL that says exactly where a resource lives, a relative URL resolved against a base that must be known, or a fragment pointing inward at a contained resource. Inside a Bundle, resolution looks locally first, converting relative references using the referring entry's own fullUrl. Inside a transaction, placeholder urn:uuid: identities and conditional search references are rewritten by the server as it assigns real ids, across references, ids, uri-typed elements, and narrative links alike.
Get those rules straight and most reference problems become obvious rather than mysterious. Then make unresolved references something you measure rather than something you discover in production: open the file in the FHIR Bundle Explorer, look at what did not resolve, and decide whether each gap is expected — with the patient data never leaving your machine.