Terminology & Code Mapping References
Every outbound result carries a claim about identity: this quantity is this analyte, drawn from this specimen, measured in this unit. The terminology service is the single component that makes that claim standardized and reproducible. It maintains the crosswalks that translate the laboratory’s local vocabulary — instrument-emitted test codes, in-house specimen abbreviations, analyzer unit strings — into the codes an external system understands: LOINC for the analyte, SNOMED CT for the specimen, and UCUM for the unit. Treated casually, these mappings live in a spreadsheet a technologist edits between other duties; treated as engineering, they are a version-controlled, machine-readable reference artifact that every release composer reads before it emits a single code, and whose exact version is stamped onto the payload so the mapping can be replayed against an inspector’s question years later. This reference specifies that artifact: its schema, its Pydantic v2 models, the deterministic version-pinned lookup, the taxonomy of mapping failures, and the coverage tests that prove no configured analyte can leave the laboratory uncoded.
Context and Pipeline Position
The terminology service is a shared read-only dependency of the Result Reporting, Release & Interoperability tier. It holds no result state and makes no clinical decision; it answers exactly one kind of question — what standardized code does this local identifier resolve to, under the mapping version in effect? — and it answers deterministically. The Report Generation & Part 11 E-Signatures composer consults it to print the LOINC and specimen on a signed report; the FHIR R4 Result Publishing composer consults it to populate Observation.code, Observation.specimen, and the UCUM unit on Observation.valueQuantity; and the Audit Trail Export & Retention subsystem records the mapping version alongside each release so an exported audit slice is self-describing.
Because all three composers read the same crosswalk, an analyte resolves to the same LOINC on the signed PDF, in the FHIR bundle, and in the audit record — divergence between the three is impossible by construction rather than by discipline. The identity the service resolves is the same one established upstream at ingestion through the Test Code Taxonomy & Standards governance; this service closes the loop, so the analyte named on the way in is the analyte coded on the way out.
The service is stateless with respect to results: it caches the crosswalk in memory, keyed by mapping version, and every lookup is a pure function of the local identifier and the pinned version. That purity is what makes it cheap to fan out across the release composers and trivial to replay during an audit.
Stage Boundaries
The terminology service exposes one ingress contract and two egress contracts, and the discipline at each edge is identical to the rest of the release tier: a positive, unambiguous mapping is the only path to a coded output, and its absence never resolves to a fabricated code.
Ingress — the local identifier plus a pinned version. A caller presents a local analyte code (or specimen abbreviation, or unit string), the mapping kind it wants resolved, and the mapping version the release run is pinned to. The version is mandatory: a lookup that omits it and silently reads “latest” would make two results released minutes apart carry different codes if the crosswalk changed between them. Pinning the version at the top of a release run makes the whole run reproducible.
Egress A — the resolved code. On a hit the service returns the standardized code (2951-2), the coding system URI, the human-readable display, and the mapping version that produced it. The version travels with the code all the way to the payload and the audit record, so the answer to “which LOINC did this result carry, and why?” is a lookup, not an investigation.
Egress B — the review hold. When no active mapping exists for the identifier under the pinned version — an unmapped local code, a mapping that was retired before the run’s effective date, or two candidate mappings with no deterministic winner — the service does not guess. It returns an unresolved outcome carrying a machine-readable reason code, and the release composer routes the result to a human-adjudicated hold. A result that cannot be coded is held, never emitted with a nearest-neighbor LOINC.
Failure semantics. A transport failure (crosswalk store unreachable, deserialization error) is retryable and must never degrade into an unresolved-but-releasable state; the lookup raises and the caller re-drives. A semantic failure (no active mapping) is not retryable — retrying resolves to the same emptiness — so it routes straight to review. The invariant across both edges matches the tier as a whole: the absence of a positive mapping never produces an outbound code.
The Crosswalk Schema
A crosswalk is a set of typed, version-controlled rows, not a lookup dictionary. Each row binds one local identifier to one standardized code within an effective window, so the crosswalk that was live at a result’s release time can be reconstructed exactly. The field contract below is the schema for a single analyte mapping row; specimen and unit crosswalks share the same shape with their coding system swapped.
| Field | Type | Semantics |
|---|---|---|
mapping_id |
string | Stable primary key for the row; referenced by audit records. |
kind |
enum | analyte, specimen, or unit — selects the coding system. |
local_code |
string | The laboratory’s instrument or in-house identifier (the lookup key). |
local_display |
string | Human-readable local name, for review and inspection. |
system |
string | Coding system URI (http://loinc.org, SNOMED CT, or UCUM). |
code |
string | The standardized code emitted onto the payload. |
display |
string | Canonical display string for the standardized code. |
unit_ucum |
string? | For analyte rows, the expected UCUM unit; null for specimen rows. |
status |
enum | active, deprecated, or retired — governs eligibility. |
effective_from / effective_to |
date | Version window; a lookup selects the row live at the run’s date. |
mapping_version |
string | Crosswalk release tag the row belongs to (tc-2026.07). |
approved_by |
string | Signer of the dual-approval clinical edit that admitted the row. |
The LOINC analyte crosswalk below is a representative slice of a basic metabolic panel. The codes are real LOINC identifiers and their canonical UCUM units; the local_code column is the laboratory’s instrument-emitted vocabulary that the crosswalk translates.
local_code |
local_display |
LOINC code |
LOINC display |
unit_ucum |
status |
|---|---|---|---|---|---|
NA |
Sodium, serum | 2951-2 |
Sodium [Moles/volume] in Serum or Plasma | mmol/L |
active |
K |
Potassium, serum | 2823-3 |
Potassium [Moles/volume] in Serum or Plasma | mmol/L |
active |
CL |
Chloride, serum | 2075-0 |
Chloride [Moles/volume] in Serum or Plasma | mmol/L |
active |
CO2 |
Bicarbonate, serum | 2028-9 |
Carbon dioxide [Moles/volume] in Serum or Plasma | mmol/L |
active |
BUN |
Urea nitrogen | 3094-0 |
Urea nitrogen [Mass/volume] in Serum or Plasma | mg/dL |
active |
CREA |
Creatinine, serum | 2160-0 |
Creatinine [Mass/volume] in Serum or Plasma | mg/dL |
active |
GLU |
Glucose, serum | 2345-7 |
Glucose [Mass/volume] in Serum or Plasma | mg/dL |
active |
CA |
Calcium, total | 17861-6 |
Calcium [Mass/volume] in Serum or Plasma | mg/dL |
active |
The SNOMED CT specimen crosswalk maps the laboratory’s local specimen abbreviations to SNOMED CT specimen concepts. Specimen coding is separate from analyte coding because the same analyte can be measured in different matrices, and the specimen must be coded independently on the FHIR Observation.specimen reference.
local_code |
local_display |
SNOMED code |
SNOMED display |
status |
|---|---|---|---|---|
SER |
Serum | 119364003 |
Serum specimen | active |
PLAS |
Plasma | 119361006 |
Plasma specimen | active |
WB |
Whole blood, venous | 122555007 |
Venous blood specimen | active |
UR |
Urine, random | 122575003 |
Urine specimen | active |
CSF |
Cerebrospinal fluid | 258450006 |
Cerebrospinal fluid specimen | active |
BLD |
Blood, unspecified | 119297000 |
Blood specimen | deprecated |
Note the deprecated blood row: it is retained so historical results that carried it remain reproducible, but it is no longer an eligible target for new mappings — the discipline of never deleting a mapping is what keeps a five-year-old released result explainable.
Implementation Patterns
Model the crosswalk row with Pydantic v2 so a malformed mapping — an inverted effective window, a retired row presented as a lookup target, a missing coding system — fails at load time rather than at release. The lookup itself is a pure function of the local code and the pinned version; the store is the only component that touches I/O.
from __future__ import annotations
import datetime as dt
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, model_validator
class Kind(str, Enum):
ANALYTE = "analyte"
SPECIMEN = "specimen"
UNIT = "unit"
class Status(str, Enum):
ACTIVE = "active"
DEPRECATED = "deprecated"
RETIRED = "retired"
class MappingRow(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
mapping_id: str
kind: Kind
local_code: str
local_display: str
system: str # coding system URI
code: str
display: str
unit_ucum: str | None = None
status: Status = Status.ACTIVE
effective_from: dt.date
effective_to: dt.date | None = None # None => open-ended
mapping_version: str
approved_by: str = Field(min_length=1)
@model_validator(mode="after")
def _check_window(self) -> "MappingRow":
if self.effective_to is not None and self.effective_to <= self.effective_from:
raise ValueError(
f"{self.mapping_id}: effective window "
f"[{self.effective_from}, {self.effective_to}) is empty or inverted"
)
if self.kind is Kind.ANALYTE and self.unit_ucum is None:
raise ValueError(f"{self.mapping_id}: analyte mapping requires a UCUM unit")
return self
The resolved outcome is itself a typed, serializable model, because it is written to the audit sink and must round-trip byte-for-byte. A hit carries the code and the version that produced it; a miss carries a stable reason code and nothing else.
class MappingHit(BaseModel):
model_config = ConfigDict(frozen=True)
local_code: str
kind: Kind
system: str
code: str
display: str
unit_ucum: str | None
mapping_version: str
class MappingMiss(BaseModel):
model_config = ConfigDict(frozen=True)
local_code: str
kind: Kind
reason: str # unmapped_code | ambiguous_mapping | retired_code
mapping_version: str
Resolution = MappingHit | MappingMiss
The resolver isolates the store behind a Protocol, pins the version once, and selects deterministically. Candidate rows are filtered to the active status and the effective window that contains the run date; zero survivors is an unmapped miss, and more than one active survivor is an ambiguity that routes to review rather than picking arbitrarily.
from typing import Protocol
class CrosswalkStore(Protocol):
async def candidates(
self, *, kind: Kind, local_code: str, mapping_version: str
) -> list[MappingRow]: ...
class TerminologyResolver:
def __init__(self, store: CrosswalkStore, *, mapping_version: str) -> None:
self._store = store
self._version = mapping_version # pinned for the whole release run
async def resolve(
self, *, kind: Kind, local_code: str, as_of: dt.date
) -> Resolution:
rows = await self._store.candidates(
kind=kind, local_code=local_code, mapping_version=self._version
)
active = [
r for r in rows
if r.status is Status.ACTIVE
and r.effective_from <= as_of
and (r.effective_to is None or as_of < r.effective_to)
]
if not active:
reason = "retired_code" if rows else "unmapped_code"
return MappingMiss(
local_code=local_code, kind=kind,
reason=reason, mapping_version=self._version,
)
if len(active) > 1:
return MappingMiss(
local_code=local_code, kind=kind,
reason="ambiguous_mapping", mapping_version=self._version,
)
row = active[0]
return MappingHit(
local_code=row.local_code, kind=row.kind, system=row.system,
code=row.code, display=row.display, unit_ucum=row.unit_ucum,
mapping_version=row.mapping_version,
)
Two design choices earn their keep here. First, the version is captured in the resolver’s constructor, not passed per call, so a single release run cannot straddle two crosswalk versions no matter how long it takes. Second, the miss distinguishes retired_code (rows existed but none active) from unmapped_code (no rows at all), because the two demand different operator responses — a retired code is a mapping-lifecycle question, an unmapped code is a coverage gap.
Error Classification and Handling
Mapping failures are tiered so each has exactly one correct disposition. Collapsing them — treating a retired code the same as a store outage — is how a laboratory ends up emitting a stale or wrong LOINC.
| Tier | Example | Disposition | Retryable |
|---|---|---|---|
| Transport | Crosswalk store timeout, deserialization error | Raise, quarantine, re-drive with backoff | Yes |
| Unmapped code | Local code with no crosswalk row under the pinned version | Review hold; open a mapping-coverage ticket | No |
| Ambiguous mapping | Two active rows match the same local code | Review hold + configuration alarm | No |
| Retired code | Rows exist but the active one was retired before the run date | Review hold; escalate to terminology owner | No |
The unmapped code is the ordinary coverage gap: a new assay went live on the instrument before its crosswalk row was approved. It is not an error in the pipeline — it is a signal that the mapping catalog trails the menu, and it must be visible as a coded review reason so a dashboard can distinguish it from noise. The ambiguous mapping is always a configuration defect: two active rows for one local code under one version means the crosswalk itself is inconsistent, so the resolver refuses to pick and raises an alarm on the crosswalk rather than the result. The retired code is the subtle one: a mapping the laboratory deliberately withdrew (a LOINC that LOINC itself deprecated, a specimen concept SNOMED CT superseded) must stop producing outbound codes at its effective_to date, but the row is retained so results released before that date still resolve on replay. Deleting a retired row would break historical reproducibility; keeping it active would emit a withdrawn code — the effective-dated status field threads between the two.
The controlling invariant mirrors the rest of the tier: any unresolved mapping produces a review hold, never a guessed code. A missing or ambiguous mapping is a gap to be closed by governance, not a value to be approximated.
Regulatory Touchpoints
Terminology binding is a compliance surface because the code is part of the test’s identity, and an interoperable payload with the wrong code is a misidentified result even when every other field is correct.
- CLIA §493.1291(a) requires that a test report identify the test performed; the LOINC carried on the FHIR
Observation.codeis that identification when the result crosses into an EHR. A crosswalk that resolves a local code to the wrong LOINC produces a report that names the wrong test, regardless of the correctness of the numeric value. Pinning and recording themapping_versionis what lets the laboratory prove which analyte identity a released result asserted. - CLIA §493.1291© requires results to be reported with their unit of measure; the UCUM binding on each analyte row is the machine-checkable form of that requirement, and the resolver’s rule that an analyte mapping must carry a
unit_ucumenforces it at load time. - 21 CFR Part 11 §11.10 demands attributable, reproducible electronic records. Because every release stamps the
mapping_versiononto the payload and the audit record, the coding decision is attributable to a specific, dual-approved crosswalk release — the same governance the Test Code Taxonomy & Standards reference establishes for analyte identity at ingestion, extended through to release. - HL7 v2 and FHIR R4 interoperability accuracy. SNOMED CT and LOINC are the semantic contracts the receiving system relies on; a payload that is structurally valid but semantically miscoded is the failure mode terminology binding exists to prevent, and it is why the FHIR publisher asserts a resolved LOINC and a UCUM unit as invariants rather than trusting the inbound identifier.
Because these clauses attach to the identity the result asserts, the audit event for a release must record the local code, the resolved standardized code, and the mapping version together, so the whole coding decision — not just its output — is reconstructable.
Testing and Validation
A crosswalk’s correctness is not provable by spot-checking a few rows; it is provable by asserting invariants over the whole artifact in CI, so a bad edit is caught before it reaches a release run. Three test classes cover it.
A coverage test loads the production crosswalk and asserts that every local code the instrument menu can emit has exactly one active mapping under the current version. This is the test that catches the unmapped-code gap before an instrument does — run it on every menu change and every crosswalk edit.
import pytest
def test_every_menu_code_maps_exactly_once(crosswalk_rows, instrument_menu):
active = [r for r in crosswalk_rows if r.status is Status.ACTIVE]
by_key: dict[tuple[Kind, str], int] = {}
for r in active:
by_key[(r.kind, r.local_code)] = by_key.get((r.kind, r.local_code), 0) + 1
missing = [
code for code in instrument_menu.analyte_codes
if (Kind.ANALYTE, code) not in by_key
]
ambiguous = [key for key, n in by_key.items() if n > 1]
assert not missing, f"unmapped analyte codes: {missing}"
assert not ambiguous, f"ambiguous mappings: {ambiguous}"
A contract test asserts the structural invariants that make the crosswalk safe to release against: no active row carries a retired coding system, every analyte row has a UCUM unit, and no two active rows for one local code have overlapping effective windows. A round-trip test confirms that a resolved MappingHit written to the audit sink deserializes back identically, because the inspector’s reconstruction depends on it.
@pytest.mark.asyncio
async def test_resolution_is_version_pinned(stub_store):
resolver = TerminologyResolver(stub_store, mapping_version="tc-2026.07")
hit = await resolver.resolve(
kind=Kind.ANALYTE, local_code="NA", as_of=dt.date(2026, 7, 16)
)
assert isinstance(hit, MappingHit)
assert hit.code == "2951-2"
assert hit.unit_ucum == "mmol/L"
assert hit.mapping_version == "tc-2026.07"
assert MappingHit.model_validate_json(hit.model_dump_json()) == hit
Property-based tests with hypothesis add the invariant that resolution is a function — the same local code and version always resolve to the same outcome — which is the property an audit replay silently relies on. Golden-file fixtures pin real analyte scenarios (a sodium that resolves to 2951-2, a retired blood specimen that produces a retired_code miss) as input/expected-outcome pairs, so a change in resolution logic surfaces as a reviewable diff. The concrete build of this artifact — the versioned table, the loader, and the coverage tests end to end — is walked through in Building a LOINC / SNOMED CT Code-Mapping Reference.
Frequently Asked Questions
Why pin a mapping version instead of always reading the latest crosswalk?
Because reproducibility depends on it. If a lookup silently read “latest”, two results released minutes apart could carry different codes when the crosswalk changed between them, and a replay years later would resolve today’s mapping rather than the one that was live at release. Pinning the version once per release run and stamping it onto every payload makes the coding decision reconstructable.
What happens when a local code has no active mapping?
The resolver returns an unresolved outcome with a machine-readable reason (unmapped_code), and the release composer routes the result to a human-adjudicated hold. It never emits a nearest-neighbor or guessed LOINC. An unmapped code is a coverage gap to be closed by governance, surfaced as a coded review reason so it is visible on a dashboard.
Why keep retired and deprecated rows instead of deleting them?
Deleting a mapping would break the reproducibility of results released while it was active. A retired row stops producing outbound codes at its effective_to date but stays in the crosswalk so a result released before that date still resolves on replay. The effective-dated status field is what lets a withdrawn code stop being emitted without erasing the history that used it.
Should specimen coding use LOINC or SNOMED CT?
SNOMED CT. LOINC identifies the analyte and, on the outbound payload, the observation; the specimen matrix is coded independently with a SNOMED CT specimen concept (for example 119364003 Serum specimen) on the FHIR Observation.specimen reference. Keeping the specimen crosswalk separate from the analyte crosswalk is what lets the same analyte be coded correctly across different matrices.
Related
- Building a LOINC / SNOMED CT Code-Mapping Reference — the concrete build of the versioned crosswalk table, loader, lookup, and coverage tests.
- FHIR R4 Result Publishing — the composer that consumes resolved LOINC, SNOMED CT, and UCUM codes to populate an
Observation. - Report Generation & Part 11 E-Signatures — the signed-report composer that prints the same resolved codes onto the human-readable report.
- Test Code Taxonomy & Standards — the ingestion-side governance of the analyte identity this service resolves at release.
- How to Map LOINC Codes to LIMS Test Panels — panel-level LOINC assignment that seeds the analyte crosswalk rows.