
FHIR Resources in Practice, Not in the Spec

The FHIR spec is thorough to the point of being overwhelming. This post cuts through the reference material and focuses on what actually matters when you're building a production healthtech system with FHIR R4.
What the Spec Gets Right and Where It Quietly Lets You Down
FHIR R4, published by HL7 in 2019, is the most implementable version of the standard to date. The RESTful API model is sensible, the resource definitions are well-structured, and the separation between the resource layer and the terminology layer (SNOMED CT, LOINC, ICD-10) is clean in theory.
In practice, the spec leaves a lot of room for interpretation, and that room is where integration projects stall.
Take the Patient resource. It has 26 top-level elements. Of those, exactly one is required: resourceType. Everything else, including name, birthDate, and identifier, is optional at the spec level. That is intentional. The spec is designed to be profiled, not used raw. But unless your implementation guide (IG) is written and enforced before you start building, every system you integrate with will make different optionality decisions, and you'll spend weeks writing defensive null-checks that should have been schema-level constraints.
The Must Support flag helps, but it only means "you must be able to handle this element if it's present." It does not mean the element will be present. That distinction bites teams repeatedly.
Which FHIR Resources Actually Appear in Real Systems?
The spec defines over 140 resource types. In production systems, you'll deal with maybe 20 of them regularly. The rest are either niche or not yet widely adopted.
| Resource | Typical use | Common pain points |
|---|---|---|
Patient |
Demographics, MRN mapping | Duplicate detection, identifier system conflicts |
Observation |
Lab results, vitals, survey scores | Code system mismatches (LOINC vs local codes) |
Condition |
Problem list, diagnosis | clinicalStatus vs verificationStatus confusion |
MedicationRequest |
Prescriptions | Dosage instruction structure is deeply nested |
Encounter |
Visit records | Period vs status transitions |
DiagnosticReport |
Radiology, pathology | Loose coupling to Observation causes reassembly pain |
DocumentReference |
Clinical notes, PDFs | Binary vs base64 vs URL attachment patterns vary widely |
Practitioner / PractitionerRole |
Provider identity | Often split incorrectly; role context gets lost |
Organization |
Facility hierarchy | Recursive partOf references need depth limits |
AllergyIntolerance |
Allergy records | code often under-coded; narrative-only entries common |
The Observation resource deserves special attention. It handles an enormous range of clinical data, from a blood glucose reading to a PHQ-9 depression score. The value[x] polymorphic type means your deserialiser needs to handle valueQuantity, valueCodeableConcept, valueString, valueBoolean, and several others, often for the same logical field depending on source system.
Why Does Terminology Cause More Bugs Than the Resource Structure?
Because the resource structure is at least enforced by your parser. Terminology failures are silent.
A Condition resource with code.coding.system = "http://snomed.info/sct" and code.coding.code = "44054006" is perfectly valid FHIR. But if the receiving system's value set only accepts ICD-10-CM codes, that condition will either be silently ignored or mapped incorrectly. No HTTP error. No validation failure. Just a missing diagnosis.
The standard terminology services pattern uses $validate-code and $translate operations against a terminology server such as HAPI FHIR's built-in terminology service or an external one like Ontoserver. Teams often skip this in favour of static lookup tables. That works until a code system updates, which SNOMED CT does twice a year and LOINC does roughly annually.
Practical approach: maintain a concept map (ConceptMap resource) for every cross-system translation you rely on, version it alongside your IG, and run automated checks against it on every release. It's not glamorous, but it's the only way to catch drift before production.
/// Not sure where to start?
Get the architecture before you commit
Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.
How Should You Handle References Between Resources?
FHIR references can be literal URLs, logical references (by identifier), or contained resources. Each has a legitimate use case and a common misuse.
Literal References
"reference": "Patient/12345" works well within a single server. Across servers, you need the full URL: "reference": "https://fhir.example.com/Patient/12345". Teams frequently use relative references in data that gets exported or shared externally and then wonder why resolving the reference fails.
Logical References
"identifier": { "system": "...", "value": "..." } without a reference field is the right pattern when you can't guarantee the referenced resource exists on the same server. CDA-to-FHIR conversions often produce logical references because the source document only carries identifiers, not server URLs. Consuming systems need to resolve these explicitly, which adds a lookup step most generic FHIR clients won't do automatically.
Contained Resources
"contained": [...] bundles a resource inside another. It's appropriate for data that has no independent existence and won't be referenced from elsewhere. It's frequently overused as a shortcut to avoid proper resource creation. Contained resources can't be searched independently, can't be referenced from outside their container, and make bundles heavier than necessary.
A good rule: if you'd ever want to query for that contained resource directly, don't contain it.
Building a Validation Pipeline That Catches Real Errors
The FHIR validator from HL7 (the Java CLI tool, available at https://github.com/hapifhir/org.hl7.fhir.core) is the reference implementation. Run it against every resource your system produces before it leaves your boundary. Not occasionally. Every time, in CI.
The validator takes an IG as input. If you're targeting US Core 6.1.0 or AU Core, pass the relevant package. If you're building a custom IG, publish it to a local package registry and point the validator there. The -ig flag accepts package IDs, directory paths, or URLs.
Common validator output categories to triage:
- Error: Fix before release. Structural or required-element violations.
- Warning: Investigate. Often flagging Missing
Must Supportpopulation or unrecognised extensions. - Information: Usually safe to suppress after review. Narrative generation notices, hint-level code system suggestions.
Suppressions should be explicit and reviewed quarterly. Blanket suppression of warnings is how silent data loss happens six months later.
Beyond the reference validator, HAPI FHIR's FhirValidator class is useful for in-process validation in Java-based backends. It's slower than schema validation alone but catches profile conformance issues that JSON Schema can't see.
Conclusion
FHIR R4 gives you a solid foundation. What it doesn't give you is a finished integration. The resource model, the terminology layer, the reference patterns, and the validation tooling all require deliberate decisions that the spec intentionally leaves to implementers.
The single most valuable step you can take before writing any integration code is drafting your implementation guide first, even a lightweight one. Define which resources you'll use, which elements are Must Support, which code systems you accept, and which reference patterns you'll enforce. That document becomes your source of truth for validation, for onboarding new team members, and for debugging integration failures six months after go-live.
If you're mid-build and don't have one yet, write it now based on what you've actually built. It's faster than it sounds and more useful than you'd expect.
FAQ
What's the difference between FHIR R4 and FHIR R4B?
R4B, published in 2022, is a patch release that fixed specific resource definitions, particularly around Citation, Evidence, and subscription backport support. For most clinical data exchange use cases, R4 and R4B are interchangeable. Check your implementation guide and target systems before choosing; mixing versions in the same pipeline causes validator confusion.
Do I need a full FHIR server or can I just produce FHIR-formatted JSON? You can produce conformant FHIR JSON without running a FHIR server. Many EHR integration patterns push FHIR bundles over standard HTTPS without a FHIR-native endpoint. The server becomes necessary when you need searchable resource storage, $operation support, or subscription notifications. Start with what your use case actually requires.
What's the fastest way to validate FHIR resources during development? The HL7 Java validator CLI is the most accurate option. For rapid feedback during development, the Simplifier.net web validator works well for one-off checks. For CI pipelines, wrap the Java CLI in a Docker container and call it as a step. HAPI FHIR's built-in validator is a good choice if your backend is already Java-based.
Why do two FHIR-compliant systems still fail to interoperate?
Because compliance means conforming to the base spec, not to a shared profile. Two systems can both be FHIR R4 compliant and still produce resources the other can't process, due to different code system choices, different Must Support interpretations, or different extension usage. Shared interoperability requires a shared implementation guide, not just shared compliance.
When should I use a FHIR Bundle versus individual resource endpoints?
Use a transaction or batch Bundle when you need atomicity or want to send related resources together in a single request. Use individual endpoints when you're creating or updating a single resource and don't need that grouping. Document bundles are for clinical documents with a fixed snapshot in time. Mixing these patterns inconsistently is a common source of partial-write bugs.
Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.
/// Work with us
Talk to the engineers who'd build it
You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.
