12. Routing follow-up queries (reads & writes)

12.1 The four White-Paper approaches, mapped to openEHR

The Intermediaries White Paper enumerates four ways a façade can determine which source server owns a resource so it can route a follow-up request. Mapped to openEHR:

WP approach What it is Verdict for openEHR

#1 - ask all servers

Broadcast the follow-up; all but the owner return 404.

Fallback only. Works because openEHR uids are UUID-based, but wasteful; the WP notes it is not seen in production.

#2 - re-identify everything

Façade keeps a persistent id-mapping table and rewrites ids in and out.

Available for full-masking deployments, but it rewrites identifiers - undesirable for openEHR uids.

#3 - system prefixes

Prefix a 1–3 char code onto ids to encode the source.

Discouraged for uids - prefixing would corrupt the object_id::creating_system_id::version_tree_id structure of an OBJECT_VERSION_ID.

#4 - microservices + fullUrl

A per-source sub-path; source encoded in the returned URL.

Recommended, combined with the openEHR-native key below.

This spec recommends #4 (per-node sub-paths) combined with the openEHR-native creating_system_id. Approach #2 stays available for deployments wanting full identifier masking, #1 is the fallback, and #3 is discouraged for uids. (The Tier MAY still use short codes for endpoint addressing, just never inside a uid.)

12.2 The openEHR-native routing key

Every openEHR VERSION carries an OBJECT_VERSION_ID = object_id '::' creating_system_id '::' version_tree_id. The creating_system_id identifies the system that created the version, which is the CDR that owns it. So:

  • A result row including a c/uid/value, a COMPOSITION/VERSION uid, already contains the owning system id. No separate routing token is needed, and no uid is ever rewritten (N22).

  • The Tier’s registry (N21) maps creating_system_id/system_id → CDR base URL. The registry MUST include every observed creating_system_id, not just node `system_id`s, because a CDR can ingest versions created elsewhere (imported compositions, §10.2).

12.3 Follow-up reads (normative)

To read a specific COMPOSITION/VERSION from a result row, the Tier resolves the owning CDR in priority order (N22):

  1. the creating_system_id parsed from the row’s OBJECT_VERSION_ID, looked up in the registry;

  2. else the endpoint_id carried on the row (if endpoint provenance was selected, §9);

  3. else an ask-all fallback (WP #1).

The Tier then forwards the read to that CDR’s base URL and returns the result. openEHR uids MUST NOT be mutated in the request or the response.

12.4 Follow-up writes (normative)

  • A versioned write (a CONTRIBUTION/commit against an existing object) MUST be routed to the single controlling CDR - the one whose system_id equals the creating_system_id of the target object. The Tier MUST reject (400) a write it cannot unambiguously route (N23).

  • New-object creation MUST target an explicitly chosen node - named via FROM ENDPOINT or the equivalent openEHR-federation-endpoint header (§8). Creating a new object that spans multiple nodes is disallowed in v1 (§2.3), matching the White Paper’s "Handling new records" caution that a façade needs a definite rule and multi-server references might not be allowed.

  • On every write, the Tier MUST convey the authenticated client identity to the target CDR (N24, §13).

  • A write derived from a de-duplicated row is routed by these same rules, and MUST NOT be sent to a node that merely holds an imported copy - see the hazard in §10.3.

  • The explicitly chosen node for a new object MAY be named by the FROM ENDPOINT directive, by the openEHR-federation-endpoint header, or by the request path (§8.4); for a non-AQL write the header or path is the only available mechanism.

12.5 Routing {base}/v1/ehr/{ehr_id}/… and the ehr_id collision problem

Every ITS-REST resource below {base}/v1/ehr/{ehr_id} is addressed by an ehr_id in the path. Unlike an OBJECT_VERSION_ID, an ehr_id is a bare HIER_OBJECT_ID and carries no system component. It is meaningless outside the CDR that issued it (§ Key terms). So a PUT {base}/v1/ehr/7f4c…/composition/… arriving at a gateway is, on its face, ambiguous.

12.5.1 How the gateway resolves it (normative)

  • The gateway MUST determine the owning node for a path ehr_id in this priority order, and MUST NOT skip to a later step while an earlier one yields an unambiguous answer:

    1. An explicit target on the request, the openEHR-federation-endpoint header (§8.4). Step 1 is the unambiguous case and is RECOMMENDED for all clients doing follow-ups: the row the client is acting on carried its endpoint_id (§9.3), so the client already knows the answer and should say so.

    2. A resolution binding the gateway holds for this client session, the {node, ehr_id} set from a preceding Step-1 resolution (§5.2).

    3. The gateway’s ehr_id → node index, where it maintains one.

    4. An ask-all probe (GET {base}/v1/ehr/{ehr_id} at each member node), for reads only.

    (N41.)

  • For writes, ask-all MUST NOT be used to disambiguate. If steps 1–3 do not yield exactly one node, the gateway MUST reject with 400 and require an explicit target, because a write destination is not something to find by trial. (N41.)

12.5.2 When two nodes claim the same ehr_id

Because ehr_id values are node-local, nothing in openEHR prevents two CDRs from holding the same ehr_id for two different patients. Since ehr_id s are UUIDs this is vanishingly rare in practice, but it remains possible.

  • If an ask-all probe (§12.5.1 step 4) finds the same ehr_id at more than one node, the gateway MUST NOT choose between them. It MUST fail the request with 409 Conflict, listing the claiming endpoints, and MUST raise this as an operational incident to the federation operator, because a collision is a federation integrity defect and not a routine client error. (N42.)

  • The gateway MUST NOT use "the node where this patient resolved" to break a collision tie unless resolution produced exactly one candidate. A collision between two nodes where the patient resolves at both is the case where guessing is most dangerous.

  • A collision detected on a read is equally reportable, because returning either node’s data would mean returning an unidentified patient’s record.

12.5.3 Prevention belongs to membership

Rules for joining a federation, which prevent these collisions in the first place, are drafted in §12b.2.

12.6 Definitions, templates and stored queries

An application that stores a template at one node and then commits a COMPOSITION against that template at another node gets a validation failure, because the second node has never seen the template. A transparent façade hits this failure mode easily, since the client believes it is talking to one repository.

  • A gateway MUST route each {base}/v1/definition/… request (template upload/retrieval, stored-query management) to a single, explicitly chosen node (§7a.1). It MUST NOT pick a node implicitly, and it MUST NOT present a union of nodes' templates as though it were one catalogue, because a client cannot act on a catalogue whose entries live in different places. (N43.)

  • A gateway MAY support fan-out template upload: a single PUT of an OPT/ADL template is applied to all member nodes (or to a named subset), so that a subsequent COMPOSITION commit validates wherever it lands. If offered, it MUST be:

    1. opt-in and explicit: a distinct request (RECOMMENDED: openEHR-federation-endpoint: *, or an explicit multi-endpoint list), never the default reading of a plain PUT;

    2. reported per node: the response MUST carry a per-endpoint outcome in the same shape as meta.federation.endpoints[] (§9.5), so the client sees exactly where the template now exists;

    3. not presented as atomic, because a broadcast is not a transaction. If some nodes accept and others fail, the gateway MUST return a partial-success status (207-style per-endpoint reporting, or 200 with the per-endpoint outcomes and meta.federation.complete = false) and MUST NOT roll back the successes. It MUST NOT report overall success while any node failed;

    4. declared in OPTIONS {base}/ (§7a.2).

    (N43.)

  • Template upload is the only fan-out write this specification permits, and it is permitted because a template is idempotent, identical across nodes, and not patient data. None of that holds for a COMPOSITION. The v1 prohibition on creating clinical objects across multiple nodes is unchanged (N23, §2.3): a fanned-out COMPOSITION create would produce N distinct objects with N distinct uids, all purporting to be one clinical fact.

  • Where fan-out upload is not supported, the deployment MUST have another way to keep templates consistent, whether out-of-band template distribution or a shared template repository. A gateway SHOULD surface a template-missing validation failure from a node as a node-reported error (§11.2) instead of masking it.

  • A stored query is the other artefact under {base}/v1/definition/…, and single-node routing serves it less well than it serves a template. §12.7 offers a gateway an additive alternative: hold the definition itself, and fan the query out at execution time.

12.7 A federated stored-query registry (optional)

A stored query is a named artefact: a client PUT s an AQL definition once and afterwards invokes it by name (POST {base}/v1/query/{qualified_query_name}). At a single CDR this works as expected. At a gateway routed under §12.6 it does not, for two reasons.

First, under §12.6 a stored query lives at one node, because that is where the PUT was routed, so invoking it by name returns that node’s rows through a façade that claims to answer for the federation (N1). A federated AQL query cannot be invoked by name at all, so openEHR’s only mechanism for naming a query is unavailable to the federation. Second, two nodes can hold different definitions under the same name and answer the same POST differently, and nothing in §12.6 detects or exposes that drift.

A gateway MAY close this by holding the definitions itself.

  • A gateway MAY offer a federated stored-query registry. If it does, the gateway is authoritative for the definition. A PUT {base}/v1/definition/query/{qualified_query_name}/{version} stores the AQL at the gateway, and a POST {base}/v1/query/{qualified_query_name} (ITS-REST stored-query execution) expands that stored AQL and fans it out under the ordinary rules of §7 (subject resolution, per-node ehr_id scoping, combine and annotate) exactly as if the client had submitted the text inline. Offering the registry MUST be declared in OPTIONS {base}/ (§7a.2). (N44.)

  • Versioning is mandatory. ITS-REST already versions stored queries with a semver path segment, and the registry MUST reuse it instead of inventing a parallel one. A stored version is immutable: a gateway MUST NOT overwrite a definition in place, and MUST refuse a second PUT to a {name}/{version} pair it already holds. Changing a definition means a new version. Immutability makes two things possible: a result can be reproduced, because the definition that produced it still exists unaltered, and a drift check has a fixed definition for a node’s copy to be compared against. (N44.)

  • Fan-out of the definition to nodes is OPTIONAL, and follows the template-upload model of §12.6 exactly. Where a gateway distributes a stored-query definition to member nodes, that distribution MUST be opt-in and explicit, MUST be reported per node in the meta.federation.endpoints[] shape (§9.5), MUST NOT be presented as atomic (partial success reported as partial, successes never rolled back, overall success never reported while a node failed), and MUST be declared in OPTIONS {base}/. These are the four conditions of §12.6’s template fan-out, cited by reference so that the two cannot drift apart. A stored-query definition is distributed on the same terms as a template, for the same reason: it is idempotent, identical across nodes, and carries no patient data.

    Definition fan-out is a facility of the registry, and a gateway MUST NOT offer it without offering the registry. The registry holds the authoritative copy that gets distributed, and it is what a node’s copy is checked against (drift, below). Distributing definitions with no authoritative source is not a configuration this specification describes, and OPTIONS {base}/ MUST NOT declare it. (N44, N43.)

  • Why a definition may legitimately need to exist at the nodes too. Fan-out is optional, but a node may execute a query by name locally, outside any federated request, and a deployment may want the definition auditable at the point of execution and not only at the gateway that dispatched it.

    • A stored query whose AQL carries FROM ENDPOINT targeting MUST NOT be fanned out, however. The targeting names members of the federation and is meaningless at a node: a node has no endpoint registry, no notion of the other members, and no way to honour or even parse the directive, which is a federation-only AQL extension (§8.1). A gateway MUST refuse to fan such a definition out, with an error saying why, instead of distributing a query the recipient cannot execute. It MAY still store it at the registry and execute it federated; the directive exists for that case. (N44.)

  • Drift. Fanning definitions out introduces a hazard that §12.6 did not have: the registry’s copy and a node’s copy of the same {name}/{version} can diverge, through a failed distribution, a local PUT at the node, a restore, or a node admitted after the distribution ran. The gateway SHOULD be able to report, per node, whether that node’s copy matches the registry version. It MUST NOT silently execute against a node whose copy differs from the definition it is answering under. It has two acceptable options: dispatch the registry’s AQL (the definition the client named, which is the safe default and what the registry model implies), or report the divergence. It must not dispatch a node’s differing copy and present the result as the named query’s answer. Divergence is the only failure mode the registry adds beyond §12.6, and nothing surfaces it unless the deployment checks for it. (N44.)

  • Where the registry is not offered, §12.6 is unchanged and governs: {base}/v1/definition/… routes to a single explicitly chosen node, no merged catalogue is presented, and a stored query is that node’s. A gateway is fully conformant without a registry.

  • name in the result set. §9.1 notes that ITS-REST’s name member carries [{namespace}::]{query-name} and SHOULD be emitted when answering a stored query. Under the registry a gateway MUST emit it, naming the gateway’s stored query, the definition the client invoked, and never a node’s copy. A client that got rows back for a name it did not ask for cannot detect that from the envelope unless the gateway says which name it answered. (N44.)