
Every non-trivial FHIR client eventually has to resolve references, and the naive resolver — one database read per reference — becomes the bottleneck within a week of production traffic. There are three or four techniques that turn the resolver into a bounded-cost pipeline instead. The site's Bundle reference walker is where you can watch each of them applied to a paste-in Bundle. For the wider setting, more on FHIR platform architecture has more.
Reference Shapes You Have To Handle
- Relative —
Patient/123— resolves against the current server context - Absolute URL —
https://server.example.org/Patient/123— points at a specific server - URN —
urn:uuid:xxxxxxxx-xxxx-...— used inside a transaction Bundle - Logical identifier —
Reference.identifierwith a system + value — no direct resolution - Contained —
#local-id— points at a resource embedded in the parent
Each shape has different resolution mechanics. The resolver has to dispatch on the shape before doing any work.
The Bundle-Local Index
The single biggest win in a Bundle resolver is building an in-memory index of the entries by fullUrl and by ResourceType/id. That index is O(N) to build once and O(1) to consult per reference. A Bundle with two thousand entries stays cheap to resolve if every reference hits the index first.
The Bundle reference walker does this by default — it indexes the Bundle before it walks. That is why it resolves a large Bundle in under a second where a naive walker would take seconds per reference.
The `urn:uuid:` Case
Transaction Bundles use urn:uuid: prefixes for cross-entry references that get rewritten at commit. Inside a Bundle in flight, the resolver treats those URNs as keys into the same in-memory index — the fullUrl field on each entry carries the URN, and references match against it. Contained references (#local-id) are always resolved inside the parent's contained[] array.
Cache The External Reads
For references that fall outside the Bundle, the resolver has to hit a remote server. Repeated references to the same Patient during a Bundle walk should reuse a per-run cache. Cache TTL matters — resource metadata drifts, so a cache lifetime longer than the run itself is asking for stale reads.
The right pattern is a request-scoped cache that lives for the duration of the current resolution and dies after. Persistent caches introduce staleness problems that are worse than the extra reads.
Preserve Reference Shape Rather Than Rewriting
A resolver that rewrites every reference into an absolute URL during walk loses the original shape. Callers that need the original — for audit, for re-serialization — end up reconstructing it. A resolver that keeps the shape and attaches the resolution result as a separate field preserves the information.
That is a small design choice that pays off in downstream ergonomics. For the shape-preservation pattern under partial outages, designing a reference resolver that survives partial outages covers the mechanics.
Batching External Reads
When multiple external references target resources on the same server, batching them into a single request cuts round trips. FHIR supports batch Bundles for exactly this — send a batch of GETs, receive a response Bundle with the results. For the deeper pattern, batching resolution to reduce round trips covers the specifics.
What About Contained?
Contained references are the cheap case. They resolve inside the parent's contained[] array by matching the local id. The trap is treating them the same as external references — the resolution shape is different, and the ownership semantics differ. For the mechanics, contained references vs external references: cost and safety is the entry.
The Short Version
Build a Bundle-local index. Dispatch on reference shape. Cache external reads with a short TTL. Preserve the original reference alongside the resolution. Batch external reads to the same server. The resolver stays bounded; the client stays responsive.

Sources
- HL7 canonical FHIR references chapter - HL7 canonical FHIR references chapter, evergreen reference