Circular References and How to Detect Them Safely

Editorial illustration in bauhaus-grid style depicting a reference graph with a detected cycle intercepted by a visited-set

A cycle in a FHIR reference graph is not always a bug. Some clinical scenarios legitimately produce them — a Condition that references a related Condition, an Observation panel with hasMember cycling back through component observations, a CarePlan that references a related CarePlan. Others are true bugs and cause resolvers to hang or crash. The mechanism for telling them apart is the same either way: a visited-set the walker consults before recursing. The site's Bundle reference walker shows detected cycles inline. For the wider FHIR framing, more on payer-side data platforms has more.

Where Cycles Come From

  • Two Conditions each referencing the other via relatedProblem
  • A CarePlan with partOf pointing at another CarePlan that points back
  • Observations with hasMember and derivedFrom cross-linking
  • Provenance chains that loop
  • Contained resources referencing the parent that contains them

The mechanism differs, the detection technique is the same. Treat every reference walk as a graph traversal that might revisit a node.

The Visited Set

The canonical detection: a walker maintains a set of (resourceType, id, version?) tuples for every resource visited. Before recursing into a reference, check the set. If the target is already visited, do not recurse — record the cycle and continue.

That is a few lines of code, and it turns unbounded recursion into a bounded walk. Every reference resolver worth deploying does this.

Depth Limits As A Backstop

Even with a visited set, a bug in the walker can produce deep recursion. A hard depth limit — twenty is generous — is the backstop. When the walker hits the limit, log, stop, and continue. That prevents the resolver from consuming stack indefinitely if the visited set has a bug.

Version-Specific References And Cycles

Two references to Patient/123/_history/5 are the same node in the graph — the resolver should treat them as one. Two references to Patient/123/_history/5 and Patient/123/_history/6 are different nodes — different versions of the same logical entity, and the walker should distinguish them.

That version-awareness is what a naive visited set often misses. Key the set by (type, id, version) when versions matter. For the mechanic, version-specific references: when you need them, when you don't is the entry.

Contained References And Cycles

Contained resources can reference their parent. Contained.Observation.subject → Patient/parent where the parent contains this Observation is technically valid and produces a cycle at walk time. The visited set has to include the parent id, not just external ids.

For the deeper contained mechanic, contained references vs external references: cost and safety covers the shape.

Reporting Cycles

A cycle should surface as a first-class result, not as an error. The walker returns the cycle path — the sequence of references that closed the loop — so the caller can decide whether it is a bug or a legitimate clinical structure.

Some deployments treat every cycle as a validation failure. Others tolerate cycles as long as they are bounded. The right choice is workload-specific; the walker just has to surface the cycle honestly.

When It Is Actually A Bug

  • Unbounded chain length (cycle detected past the depth limit)
  • Cycle in a workflow that logically should not have one
  • Cycle involving Provenance — usually indicates a broken chain-of-custody

Detection is the mechanism; deciding is the workflow's job. For the design that ties it into a robust resolver, designing a reference resolver that survives partial outages is the entry.

The Short Version

Every FHIR resolver needs a visited set keyed by type, id, and version. Depth limits are the backstop. Report cycles as a first-class result. Some cycles are legitimate; the workflow decides.

Bauhaus-grid diagram of a reference graph with a detected cycle highlighted, showing the visited-set intercepting the recursion, drawn as flat geometric grid tiles with purple accents on off-white

Sources