Sep 9, 2026
FHIR REST & Subscription Integration
How it works
The obvious places to point a test harness in this chain are the two ends: the EHR that emits the message and the downstream consumer that finally reads a FHIR resource off the REST API. Both are tempting because both are observable — you can send a known admission event and assert that a known resource comes back. But that framing hides where the flow actually earns its name. The HL7 Interface Listener and the Canonical Normalizer sit between an HL7 message format and a FHIR resource model, and everything that survives to the REST API has already been through a translation the endpoints cannot see. The listener decides what counts as a receivable message; the normalizer decides what that message *means* in canonical terms before the FHIR Resource Store ever persists it. A resource that arrives at the consumer correctly shaped tells you nothing about whether it was shaped correctly — only that it was shaped consistently with whatever the normalizer believed.
That is why a QA engineer's first attention belongs at the normalization boundary, not the API contract. The REST API is a read surface over the store; it faithfully serves whatever the store holds, which means every normalizer defect is laundered into a well-formed, schema-valid response that passes endpoint assertions. Subscription delivery compounds this, because the consumer is notified on the basis of what the store contains, so a mis-normalized resource doesn't just sit there waiting to be caught — it actively propagates outward to the downstream system on its own. Testing the seams means driving varied HL7 input at the listener and asserting against the canonical form directly, before the store, rather than inferring correctness from a clean 200 at the far end. The endpoints are where failures become visible; the normalizer is where they are created, and the distance between those two points is exactly the debugging cost you pay for testing in the wrong place.
Caveats — what breaks in practice
The failure mode most likely to blindside a team is the success response that doesn't correspond to a persisted record — and its close cousin, the record accepted but stuck in a staged state forever. Everything else on this list announces itself. Connection drops mid-message, timeouts on oversized batches, throttling under batch load, poison messages halting processing, validation rejecting edge-case values — these all produce a visible error, a stalled queue, or a retry storm that someone gets paged for. Encoding corruption and missing segments show up as garbled or absent data in a downstream reader. But a 2xx with no durable write leaves no artifact anywhere: the sender's logs say success, the retry logic never fires because there was nothing to retry, and the receiving system has no failed-record table entry because, as far as it's concerned, nothing failed. The same is true of a staged record that never advances — it was accepted, it exists, and it is simply not where anyone is looking.
The assumption that makes this surprising is that acknowledgement means commitment: that an ACK or a success response is a statement about durable state rather than about receipt. The list makes clear this doesn't hold — ACK is sent before persistence, so a crash in that window loses the message outright, and separately a success response can come back for a record that never lands. Replication lag compounds the illusion, because a read against a replica can return stale results that look like ordinary eventual consistency rather than a missing write, and schema drift after a migration can break a downstream reader silently, so the absence never surfaces as an exception. For QE this means reconciliation, not response-code assertion, is the real test: assert on the persisted record read from the primary after the write, assert that staged records transition out of staged within a bound, and build a periodic count-and-content comparison between what was sent and what is actually retrievable. A test suite that treats HTTP 200 as the oracle will pass cleanly through exactly the failure the team will spend a week diagnosing.
How to test this end to end
Picture an ADT^A08 from the Mercy East EHR arriving on the HL7 Interface Listener at 09:14:02: MSH-10 control ID `MRC-20240611-0918442`, PID-3 `MRN=EE-4471902`, PID-5 `HÖLZL^ANNIKA`, PID-8 `F`, PV1-44 admit datetime `202406110914`, and a PID-7 birthdate the site sends as `19850000` because only the year was captured at registration. The Canonical Normalizer maps that into a FHIR `Patient` with `identifier[0].value = "EE-4471902"`, `name.family = "Hölzl"`, `gender = "female"`, `birthDate = "1985"`, and writes it to the FHIR Resource Store as `Patient/pat-ee-4471902` at version `3`. That stored resource is the single canonical copy both delivery lanes read from — the REST consumer that later GETs `Patient/pat-ee-4471902` and the Subscription consumer that gets pushed the change are looking at the same bytes, so whatever the normalizer wrote is what both see.
Two things go wrong here and they go wrong quietly. The partial birthdate `19850000` is exactly the edge-case field value that breaks mapping: a normalizer that assumes eight digits produces `birthDate = "1985-00-00"` or drops the field entirely, and because the listener ACKs on receipt, Mercy East believes the update landed cleanly. Separately, `HÖLZL` arrives as ISO-8859-1 while the normalizer decodes as UTF-8, so the store holds `H?LZL` — a corruption that both the pull and push paths reproduce identically, so no consumer disagreement ever surfaces to flag it. To test, replay `MRC-20240611-0918442` with the raw bytes captured off the wire, then assert directly against the store that `Patient/pat-ee-4471902` has `name.family` equal to `Hölzl` and a `birthDate` that is either a valid FHIR partial date `1985` or an explicit data-absent extension — never a malformed `1985-00-00`. Then re-send the identical message to confirm the control ID is deduplicated and the version stays at `3` rather than advancing to `4`, and kill the normalizer process between ACK and commit to verify `MRC-20240611-0918442` is still recoverable rather than lost.
Downstream of the store sits the delivery seam: the Mercy East registry consumer at `https://registry.mercy-east.org/fhir` pulls `Patient/pat-ee-4471902` over REST at 09:14:07, three seconds behind the normalizer's write of version `3`. What crosses the wire is exactly what CP1 committed — `identifier[0].value = "EE-4471902"`, `name.family = "Hölzl"`, `gender = "female"`, `birthDate = "1985"` — and the registry answers `201 Created` with its own `Location: Patient/reg-88213`. The Subscription lane pushes the same version `3` bytes to the same registry endpoint moments later, so this consumer receives `pat-ee-4471902` twice by two routes; whether that lands as one record or two is entirely the target's business, not ours. Note what the checkpoint cannot fix: the `Hölzl` corruption from CP1, if present, is faithfully delivered here, because both lanes read the one canonical copy.
The failures at CP2 are target-side and they wear a `201`. A registry validator that requires a full `YYYY-MM-DD` may reject `birthDate = "1985"` outright, or worse, accept it and silently default it to `1985-01-01`, so `reg-88213` now asserts a January birthday nobody ever recorded. The `201` itself can be a lie — the record is staged pending a match review against an existing `EE-4471902` and sits there forever, never queryable. And because the REST read and the Subscription push both deliver version `3`, duplicate staged records from the same upstream message are the expected default, not the exception; add batch load from a nightly backfill and throttling will start dropping pushes while REST reads still succeed, so the two lanes diverge in coverage even though they agree on content. To test, deliver `pat-ee-4471902` and then read back `reg-88213` from the registry's own API rather than trusting the `201`: assert `birthDate` is still `1985` and not `1985-01-01`, assert `name.family` is `Hölzl` byte-for-byte, and assert the record is in an active, queryable state rather than staged. Then fire both lanes for version `3` deliberately and confirm the registry holds exactly one record for `EE-4471902`, and replay a hundred `pat-*` writes in one burst to see whether pushes for `pat-ee-4471902` get throttled away while its REST read still returns fine.
CP1 — Projection & Apply
EHR System → HL7 Interface Listener → Canonical Normalizer → FHIR Resource Store
CP2 — Target Delivery
FHIR REST API → Downstream Consumer System
Want this level of breakdown for your own system? Match your architecture in a few questions — no confidential upload required.