FHIR and openEHR: When to Use Which, and How They Coexist

“FHIR or openEHR?” is asked in most health data platform discussions, usually as if one had to win. The question hides a category error. HL7 FHIR was designed to move clinical data between systems. openEHR was designed to keep a clinical record for the lifetime of a patient. A platform has to do both, and the two standards are closer to a transport and a store than to competitors.

The overlap is still real. FHIR servers persist data. openEHR has a REST API. Both model a blood pressure. An architect has to decide which model is authoritative, where the other one is derived, and what is lost at the boundary.

This article compares the two on the dimensions that affect architecture, shows the same blood pressure in each, and gives a coexistence design with a working mapping in Python and C#. The mapping is small on purpose. Its interesting output is not the FHIR resource but the list of what the FHIR resource could not carry.

Two standards, two questions

Table comparing HL7 FHIR and openEHR across eight dimensions: purpose, unit of data, clinical model, who models, query, versioning, strengths and weaknesses.
The two standards on the dimensions that affect architecture.

FHIR defines a fixed set of resource types such as Patient, Observation and MedicationRequest, each deliberately generic. An Observation can hold almost any measurement. Specific meaning is added by profiles: an implementation guide says that a blood pressure is an Observation with a particular LOINC code and two components. FHIR follows the 80 per cent rule, so an element belongs in the base resource only if most systems use it, and everything else is an extension.

openEHR separates two levels. A small, stable reference model defines generic structures: compositions, observations, evaluations, instructions, actions, and data types. Clinical content is defined above it in archetypes, each of which aims to describe everything that could sensibly be recorded about one concept. The blood pressure archetype covers the readings, and also the patient’s position, the cuff size, the measurement site, the device and more. Templates then select and constrain archetypes for a real form or message. The software only has to implement the reference model, so new clinical content does not require new tables or a release.

That difference in philosophy produces the practical one. A FHIR profile says what must be present for an exchange to be valid. An archetype says what may be recorded so that nothing clinically relevant is lost. One is a minimum, the other a maximum.

The same blood pressure, twice

An openEHR composition in canonical JSON, trimmed to the blood pressure entry. The node identifiers such as at0004 come from the archetype and are the same in every system that uses it.

{
  "_type": "OBSERVATION",
  "archetype_node_id": "openEHR-EHR-OBSERVATION.blood_pressure.v2",
  "data": {
    "_type": "HISTORY", "archetype_node_id": "at0001",
    "events": [{
      "_type": "POINT_EVENT", "archetype_node_id": "at0006",
      "time": {"_type": "DV_DATE_TIME", "value": "2026-11-03T09:42:00+00:00"},
      "data": {"_type": "ITEM_TREE", "archetype_node_id": "at0003", "items": [
        {"_type": "ELEMENT", "archetype_node_id": "at0004", "name": {"value": "Systolic"},
         "value": {"_type": "DV_QUANTITY", "magnitude": 142, "units": "mm[Hg]"}},
        {"_type": "ELEMENT", "archetype_node_id": "at0005", "name": {"value": "Diastolic"},
         "value": {"_type": "DV_QUANTITY", "magnitude": 88, "units": "mm[Hg]"}}
      ]},
      "state": {"_type": "ITEM_TREE", "archetype_node_id": "at0007", "items": [
        {"_type": "ELEMENT", "archetype_node_id": "at0008", "name": {"value": "Position"},
         "value": {"_type": "DV_CODED_TEXT", "value": "Sitting",
                   "defining_code": {"terminology_id": {"value": "local"}, "code_string": "at1001"}}}
      ]}
    }]
  },
  "protocol": {"_type": "ITEM_TREE", "archetype_node_id": "at0011", "items": [
    {"_type": "ELEMENT", "archetype_node_id": "at0013", "name": {"value": "Cuff size"},
     "value": {"_type": "DV_CODED_TEXT", "value": "Large Adult",
               "defining_code": {"terminology_id": {"value": "local"}, "code_string": "at0016"}}},
    {"_type": "ELEMENT", "archetype_node_id": "at0014", "name": {"value": "Location of measurement"},
     "value": {"_type": "DV_CODED_TEXT", "value": "Right arm",
               "defining_code": {"terminology_id": {"value": "local"}, "code_string": "at0025"}}}
  ]}
}

The same measurement as a FHIR R4 Observation, following the standard blood pressure profile: a panel code with a systolic and a diastolic component.

{
  "resourceType": "Observation",
  "status": "final",
  "identifier": [{"system": "urn:openehr:composition-uid",
                  "value": "8f2c1a9e-3b7d-4c55-9a10-6d2e4f7b1c33::synthetic.example::1"}],
  "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category",
                            "code": "vital-signs"}]}],
  "code": {"coding": [{"system": "http://loinc.org", "code": "85354-9", "display": "Blood pressure panel"}]},
  "subject": {"reference": "Patient/pat-001"},
  "effectiveDateTime": "2026-11-03T09:42:00+00:00",
  "component": [
    {"code": {"coding": [{"system": "http://loinc.org", "code": "8480-6", "display": "Systolic blood pressure"}]},
     "valueQuantity": {"value": 142, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"}},
    {"code": {"coding": [{"system": "http://loinc.org", "code": "8462-4", "display": "Diastolic blood pressure"}]},
     "valueQuantity": {"value": 88, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"}}
  ],
  "bodySite": {"coding": [{"system": "http://snomed.info/sct", "code": "368209003",
                           "display": "Right upper arm structure"}]}
}

The readings, the time and the measurement site made the crossing. The patient’s position and the cuff size did not. FHIR could carry them with extensions or additional components, but the standard profile does not ask for them, so a receiving system will not look for them. For a hypertension clinic that distinction matters: a seated reading and a standing reading are different clinical facts.

Three other differences are visible. The openEHR record is identified by meaning, through archetype paths, where FHIR identifies meaning by terminology codes. The openEHR record has no patient reference inside it, because a composition lives inside one patient’s EHR, while every FHIR resource names its subject. And the openEHR version identifier in uid ends in a version number, because every change creates a new version by design.

Querying each

The models lead to different query styles. FHIR search is a REST call with parameters defined per resource type:

GET [base]/Observation?patient=pat-001&code=http://loinc.org|85354-9&date=ge2026-01-01&_sort=-date

openEHR uses the Archetype Query Language (AQL), which looks like SQL but navigates archetype paths, so one query works on any conformant repository regardless of its physical schema:

SELECT
  c/uid/value                                                     AS composition,
  o/data[at0001]/events[at0006]/time/value                        AS measured_at,
  o/data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude AS systolic,
  o/data[at0001]/events[at0006]/data[at0003]/items[at0005]/value/magnitude AS diastolic,
  o/data[at0001]/events[at0006]/state[at0007]/items[at0008]/value/value    AS position
FROM EHR e
  CONTAINS COMPOSITION c
  CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v2]
WHERE e/ehr_id/value = $ehr_id
  AND o/data[at0001]/events[at0006]/data[at0003]/items[at0004]/value/magnitude >= 140
ORDER BY o/data[at0001]/events[at0006]/time/value DESC

In release 1.1.0 of the openEHR REST API the query is sent as the q field of a POST to /v1/query/aql, with query_parameters alongside. Note what the AQL can ask that the FHIR search cannot without custom search parameters: the patient’s position, and a population query across every EHR by dropping the WHERE on ehr_id. Note also what FHIR gives that AQL does not: a query any FHIR client already knows how to issue, with no knowledge of archetypes.

ℹ️
THE AQL ABOVE WAS NOT EXECUTED

I had no openEHR repository available while writing. The paths use node identifiers checked against the published blood pressure archetype, and the syntax follows the AQL specification, but the query has not been run. Test it against your own repository and template before relying on it.

When to use which

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3E0','primaryTextColor':'#232F3E','primaryBorderColor':'#FF9900','lineColor':'#232F3E','clusterBkg':'#F8FAFC','clusterBorder':'#CBD5E1','edgeLabelBackground':'#FFFFFF','nodeTextColor':'#232F3E'}}}%%
flowchart TD
    Q1{What is the system's main job?}
    Q1 -- "Exchange data with other
organisations and apps" --> F[FHIR as the contract] Q1 -- "Be the long-term
clinical record" --> Q2{How rich and varied
is the clinical content?} Q2 -- "Standard summaries: problems,
medication, results" --> FS[FHIR-native store
is sufficient] Q2 -- "Detailed, evolving, specialty
content owned by clinicians" --> O[openEHR repository] O --> B[Expose it through FHIR
for exchange] FS --> B2[Add openEHR later if
content outgrows profiles] class Q1,Q2 edge class F,FS mesh class O compute class B,B2 store classDef compute fill:#FFF3E0,stroke:#FF9900,stroke-width:2px,color:#232F3E classDef edge fill:#EBF8FF,stroke:#0073BB,stroke-width:2px,color:#004C74 classDef store fill:#F0FFF4,stroke:#1D8102,stroke-width:2px,color:#145202 classDef alert fill:#FEF2F2,stroke:#D13212,stroke-width:2px,color:#991B1B classDef mesh fill:#F3E8FF,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
  • Choose FHIR as the contract for anything that crosses an organisational boundary: patient access, referrals, national summaries, app integration. The ecosystem of servers, client libraries, app-launch standards and regulation is built around it.
  • Choose a FHIR-native store when the record you keep is broadly what you exchange. A patient-facing app, a referral hub or an integration layer rarely needs more than well-profiled resources.
  • Choose openEHR for persistence when clinicians need to define and change detailed content, when several applications from different vendors must share one record, and when the data has to outlive every one of those applications.
  • Do not choose openEHR to avoid FHIR. You will still need a FHIR interface the day a partner, a regulator or an app asks for one.

How they coexist

Diagram of a coexistence architecture: clinical applications write to an openEHR repository, a mapping layer translates to a FHIR API for partners and apps, with the questions each standard answers listed below.
One authoritative repository, one mapping layer and one exchange API.

The common pattern keeps one authoritative clinical data repository and derives the exchange view from it. Applications write compositions. A mapping layer translates archetype paths into FHIR elements and local codes into standard terminologies. A FHIR API serves the result to everyone outside.

%%{init: {'theme':'base','themeVariables':{'actorBkg':'#FFF3E0','actorBorder':'#FF9900','actorTextColor':'#232F3E','signalColor':'#0073BB','signalTextColor':'#0F172A','noteBkgColor':'#FFF8E7','noteBorderColor':'#FF9900','noteTextColor':'#232F3E','labelBoxBkgColor':'#FFF3E0','labelBoxBorderColor':'#FF9900','labelTextColor':'#232F3E','loopTextColor':'#232F3E','activationBkgColor':'#EBF8FF','activationBorderColor':'#0073BB','lineColor':'#232F3E','textColor':'#232F3E'}}}%%
sequenceDiagram
    participant P as Partner system
    participant F as FHIR API
    participant M as Mapping layer
    participant O as openEHR repository
    P->>F: GET Observation?patient=..
    F->>M: Resolve patient to EHR id
    M->>O: AQL for blood pressure entries
    O-->>M: Result set by archetype path
    M-->>F: FHIR Observations + loss report
    F-->>P: Bundle conforming to the profile

There are two ways to run that mapping, and the choice is architectural.

On request (facade)On write (materialised)
How it worksEach FHIR call is translated to AQL and the result mapped backEvery committed composition is mapped once and stored in a FHIR server
FreshnessAlways currentLags by the event pipeline
FHIR searchLimited to what you translate to AQLFull search on the FHIR server
Cost of a mapping changeImmediateRe-map and reload affected data
Failure modeA slow or unsupported queryA stale or missing resource
FitsA small set of well-defined readsBroad API access, analytics, national exchange

On Azure the pieces map as follows.

RoleAzure building blockNotes
openEHR repositoryAn open-source or commercial openEHR server on Container Apps or AKS, with Azure Database for PostgreSQL where the server uses itI know of no first-party managed openEHR service on Azure. You run one or buy one
Mapping layerContainer Apps or Functions running .NET or PythonStateless. Scale it separately from both stores
FHIR API and storeFHIR service in Azure Health Data ServicesFor the materialised pattern. For a facade, your own API in front
Change feedEvent Grid, from the repository’s commit eventsCarries composition identifiers, not clinical payloads
IdentityMicrosoft Entra ID, with SMART on FHIR scopes at the FHIR APIOne identity model for both sides
Patient indexA small service or table mapping EHR id to FHIR Patient idThe mapping layer must never guess this

A mapping that reports what it loses

The sample maps the blood pressure composition shown earlier. It is hand-written for one archetype, which is not how a programme with hundreds of archetypes should work, but it makes the mechanics visible. The return value has two parts: the FHIR resources, and a report of what was mapped and what was not carried.

"""bp_mapping.py - openEHR blood pressure OBSERVATION (canonical JSON) to FHIR R4 Observation."""
from dataclasses import dataclass, field

BP_ARCHETYPE = "openEHR-EHR-OBSERVATION.blood_pressure.v2"
LOINC, UCUM, SNOMED = "http://loinc.org", "http://unitsofmeasure.org", "http://snomed.info/sct"
COMPOSITION_ID_SYSTEM = "urn:openehr:composition-uid"

# archetype node -> (LOINC code, display)
COMPONENTS = {"at0004": ("8480-6", "Systolic blood pressure"), "at0005": ("8462-4", "Diastolic blood pressure")}
# archetype-local code for Location of measurement -> SNOMED CT body site. Deliberately incomplete.
BODY_SITES = {"at0025": ("368209003", "Right upper arm structure")}


@dataclass
class MappingReport:
    mapped: list[str] = field(default_factory=list)
    not_carried: list[str] = field(default_factory=list)      # present in openEHR, absent from the FHIR output


def _items(tree: dict | None) -> dict[str, dict]:
    return {item["archetype_node_id"]: item for item in (tree or {}).get("items", [])}


def blood_pressure_to_fhir(composition: dict, patient_id: str) -> tuple[list[dict], MappingReport]:
    """One FHIR Observation per openEHR event. patient_id comes from your EHR-to-Patient index, not from here."""
    report, out = MappingReport(), []
    uid = composition["uid"]["value"]
    for entry in composition.get("content", []):
        if entry.get("archetype_node_id") != BP_ARCHETYPE:
            report.not_carried.append(f"entry {entry.get('archetype_node_id')}: no mapping defined")
            continue
        protocol = _items(entry.get("protocol"))
        for event in entry["data"]["events"]:
            values, components = _items(event.get("data")), []
            for node, (code, display) in COMPONENTS.items():
                if node not in values:
                    continue
                quantity = values[node]["value"]
                if quantity["units"] != "mm[Hg]":
                    raise ValueError(f"{node}: expected mm[Hg], got {quantity['units']}")
                components.append({
                    "code": {"coding": [{"system": LOINC, "code": code, "display": display}]},
                    "valueQuantity": {"value": quantity["magnitude"], "unit": "mmHg", "system": UCUM, "code": "mm[Hg]"},
                })
                report.mapped.append(f"{values[node]['name']['value']} -> component {code}")
            if not components:
                report.not_carried.append("event without systolic or diastolic: skipped")
                continue
            observation = {
                "resourceType": "Observation",
                "status": "final",
                "identifier": [{"system": COMPOSITION_ID_SYSTEM, "value": uid}],      # the way back to the source
                "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category",
                                          "code": "vital-signs"}]}],
                "code": {"coding": [{"system": LOINC, "code": "85354-9", "display": "Blood pressure panel"}]},
                "subject": {"reference": f"Patient/{patient_id}"},
                "effectiveDateTime": event["time"]["value"],
                "component": components,
            }
            site = protocol.get("at0014")
            if site:
                local = site["value"]["defining_code"]["code_string"]
                if local in BODY_SITES:
                    code, display = BODY_SITES[local]
                    observation["bodySite"] = {"coding": [{"system": SNOMED, "code": code, "display": display}]}
                    report.mapped.append(f"Location of measurement -> bodySite {code}")
                else:
                    report.not_carried.append(f"Location of measurement '{site['value']['value']}': no SNOMED mapping")
            for node, item in values.items():
                if node not in COMPONENTS:
                    report.not_carried.append(f"data/{item['name']['value']}")
            for item in _items(event.get("state")).values():
                report.not_carried.append(f"state/{item['name']['value']} = {item['value'].get('value')}")
            for node, item in protocol.items():
                if node != "at0014":
                    report.not_carried.append(f"protocol/{item['name']['value']} = {item['value'].get('value')}")
            out.append(observation)
    return out, report

The C# version has the same structure and uses only System.Text.Json, so it needs no FHIR or openEHR SDK. The core of it:

using System.Text.Json.Nodes;

namespace Interop;

/// <summary>What the mapping produced and, as important, what it could not carry.</summary>
public sealed record MappingResult(
    IReadOnlyList<JsonObject> Observations, IReadOnlyList<string> Mapped, IReadOnlyList<string> NotCarried);

public static class BloodPressureMapper
{
    public const string Archetype = "openEHR-EHR-OBSERVATION.blood_pressure.v2";
    private const string Loinc = "http://loinc.org";
    private const string Ucum = "http://unitsofmeasure.org";

    private static readonly (string Node, string Code, string Display)[] Components =
    [
        ("at0004", "8480-6", "Systolic blood pressure"),
        ("at0005", "8462-4", "Diastolic blood pressure"),
    ];

    /// <param name="patientId">Comes from your EHR-to-Patient index, never from the composition.</param>
    public static MappingResult ToFhir(JsonNode composition, string patientId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(patientId);
        List<JsonObject> observations = [];
        List<string> mapped = [], notCarried = [];
        string uid = Text(composition["uid"]?["value"], "composition uid");

        foreach (JsonNode? entry in composition["content"]?.AsArray() ?? [])
        {
            string? archetype = (string?)entry?["archetype_node_id"];
            if (archetype != Archetype)
            {
                notCarried.Add($"entry {archetype}: no mapping defined");
                continue;
            }

            Dictionary<string, JsonNode> protocol = Items(entry!["protocol"]);
            foreach (JsonNode? ev in entry["data"]?["events"]?.AsArray() ?? [])
            {
                Dictionary<string, JsonNode> values = Items(ev?["data"]);
                JsonArray components = [];
                foreach ((string node, string code, string display) in Components)
                {
                    if (!values.TryGetValue(node, out JsonNode? element))
                    {
                        continue;
                    }

                    string units = Text(element["value"]?["units"], $"{node} units");
                    if (units != "mm[Hg]")
                    {
                        // A unit the mapping does not expect stops the pipeline. It never passes through.
                        throw new InvalidOperationException($"{node}: expected mm[Hg], got {units}");
                    }

                    components.Add(new JsonObject
                    {
                        ["code"] = Coding(Loinc, code, display),
                        ["valueQuantity"] = new JsonObject
                        {
                            ["value"] = element["value"]!["magnitude"]!.GetValue<decimal>(),
                            ["unit"] = "mmHg",
                            ["system"] = Ucum,
                            ["code"] = "mm[Hg]",
                        },
                    });
                    mapped.Add($"{Name(element)} -> component {code}");
                }

                if (components.Count == 0)
                {
                    notCarried.Add("event without systolic or diastolic: skipped");
                    continue;
                }

                JsonObject observation = new()
                {
                    ["resourceType"] = "Observation",
                    ["status"] = "final",
                    ["identifier"] = new JsonArray(new JsonObject
                        { ["system"] = "urn:openehr:composition-uid", ["value"] = uid }),
                    ["code"] = Coding(Loinc, "85354-9", "Blood pressure panel"),
                    ["subject"] = new JsonObject { ["reference"] = $"Patient/{patientId}" },
                    ["effectiveDateTime"] = Text(ev!["time"]?["value"], "event time"),
                    ["component"] = components,
                };

                // Body site, category and the remaining loss reporting are in the repository version.
                notCarried.AddRange(Items(ev["state"]).Values
                    .Select(i => $"state/{Name(i)} = {(string?)i["value"]?["value"]}"));
                notCarried.AddRange(protocol.Where(p => p.Key != "at0014")
                    .Select(p => $"protocol/{Name(p.Value)} = {(string?)p.Value["value"]?["value"]}"));

                observations.Add(observation);
            }
        }

        return new MappingResult(observations, mapped, notCarried);
    }

    private static Dictionary<string, JsonNode> Items(JsonNode? tree) =>
        (tree?["items"]?.AsArray() ?? [])
            .Where(item => item is not null)
            .ToDictionary(item => Text(item!["archetype_node_id"], "archetype_node_id"), item => item!);

    private static string Name(JsonNode element) => (string?)element["name"]?["value"] ?? "unnamed";

    private static string Text(JsonNode? node, string what) =>
        (string?)node ?? throw new InvalidOperationException($"Missing {what}");

    private static JsonObject Coding(string system, string code, string display) =>
        new() { ["coding"] = new JsonArray(new JsonObject
            { ["system"] = system, ["code"] = code, ["display"] = display }) };
}

Both implementations print the same report for the sample composition:

mapped      -> Systolic -> component 8480-6; Diastolic -> component 8462-4; Location of measurement -> bodySite 368209003
not carried -> state/Position = Sitting; protocol/Cuff size = Large Adult
unit check  -> at0004: expected mm[Hg], got kPa

The Python sample also maps back from FHIR to openEHR. The round trip restores the two readings and the time, and nothing else: no state, no protocol. That is the practical argument for deciding which side is authoritative. Whichever model is derived should never be written back over the one it came from.

💡
HOW THIS WAS CHECKED

The Python mapping and its tests ran on Python 3.13. The full C# version in the repository compiled with warnings treated as errors and ran on the .NET 10 SDK (10.0.115), with the same assertions. The listing above is an excerpt of that file with the body-site branch removed for length. The excerpt was also compiled on its own. All data is synthetic, and neither implementation was run against a live openEHR or FHIR server.

Mapping at scale

Nobody should hand-write a few hundred of these. Two developments make that unnecessary. FHIRconnect is a declarative mapping language for openEHR and FHIR, with an open-source engine, openFHIR, that executes the mappings in both directions. Its authors reported in November 2025 that they had mapped 24 international archetypes to 15 FHIR profiles, with about 65 per cent of the mapping content reused across projects. Separately, HL7 has been publishing openEHR archetypes as FHIR logical models, which gives the two communities a shared artefact to map against.

The tooling does not remove the governance. A mapping is a clinical statement that two things mean the same. It needs an owner, a version, a review by someone who understands both models, and a test set. Treat the mapping library as you treat the archetypes and profiles themselves.

Running it in production

  • Name the source of truth. One store is authoritative for each kind of data. Derived views are read-only. Write it down, because the question returns every time a new application wants to write.
  • Keep the loss report. Log what each mapping could not carry, per archetype and per profile. A rising count is the earliest sign that the clinical content has moved ahead of the exchange profile.
  • Own the patient index. openEHR identifies an EHR, FHIR identifies a Patient. The link between them is master data with its own lifecycle, including merges.
  • Translate terminology explicitly. Archetype-local codes such as at0025 mean nothing outside the archetype. Map them to SNOMED CT or LOINC through a terminology service, and report unmapped values instead of guessing.
  • Fail on unexpected units. The sample stops on anything other than mm[Hg]. Silent conversion, or silent pass-through, is how a kilopascal reading becomes a clinical incident.
  • Version mappings with templates. A new template version can move or add nodes. Pin each mapping to the template versions it was tested against.
  • Carry provenance both ways. The FHIR resource holds the composition identifier. Keep the reverse link too, so that a correction in the repository finds every resource derived from it.
⚖️
REGULATION CONCERNS EXCHANGE, NOT YOUR STORAGE MODEL

The European Health Data Space Regulation, (EU) 2025/327, sets obligations for how electronic health data is made available and exchanged, in a European exchange format defined through implementing acts. It does not prescribe the internal data model of a record system. That leaves room for either persistence choice, provided the exchange interface conforms. Under GDPR, the repository, the derived FHIR store, the mapping logs and any event payloads all hold special-category data, so each needs the same access control, residency and retention decisions. Erasure and rectification must reach the derived copy as well as the source.

⚠️
A WRONG MAPPING IS A PATIENT SAFETY HAZARD

A mapping that drops context, swaps systolic and diastolic, or carries the wrong unit produces a record that looks valid and is clinically wrong. Mappings belong in the clinical safety case, with a hazard log entry, clinical review and test evidence for each one before it goes live.

Key takeaways

  • FHIR is designed for exchange and openEHR for persistence. Compare them on the job in front of you, not in the abstract.
  • A FHIR profile states the minimum for a valid exchange. An archetype states the maximum that may be recorded. Mapping from the second to the first always loses something.
  • Decide which store is authoritative and keep every derived view read-only.
  • Choose between mapping on request and mapping on write deliberately. They fail in different ways.
  • Make every mapping report what it did not carry, and treat mappings as governed clinical artefacts.

If you run both standards side by side, I would like to hear which direction you chose as authoritative and what the loss report taught you. Leave a comment below.

Glossary

HL7 FHIR
Fast Healthcare Interoperability Resources. A standard for exchanging health data as resources over a REST API.
openEHR
An open specification for clinical data repositories, built on a stable reference model and clinician-authored archetypes.
Reference model
The small set of generic structures and data types that openEHR software implements.
Archetype
A reusable, formal definition of one clinical concept, such as blood pressure, with all the data that may be recorded about it.
Template
A selection and constraint of archetypes for a specific form, document or message.
Composition
The openEHR unit of committed, versioned clinical content, comparable to a document.
AQL
Archetype Query Language. Queries data by archetype path, independent of the physical database schema.
Profile
A set of constraints on a FHIR resource for a particular use, published in an implementation guide.
Resource
One unit of FHIR data, such as an Observation or a Patient.
CKM
Clinical Knowledge Manager. The shared repository where openEHR archetypes are developed and reviewed.
Canonical JSON
The openEHR serialisation that mirrors the reference model exactly, with type markers on each node.
Facade
An API that translates requests to another model at request time without storing its own copy.
EHDS
European Health Data Space. EU regulation governing access to and exchange of electronic health data.
UCUM
Unified Code for Units of Measure. The unit system used by both FHIR and openEHR quantities.

References & Further Reading


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.