Healthcare/FHIR facade
Architecture pattern

Building a FHIR facade over legacy HL7 v2

A FHIR facade answers FHIR-shaped REST requests by querying legacy systems underneath - clients see FHIR, the legacy side stays untouched. When to build one and how.

0changes to the legacy system
RESTwhat clients see
HL7 v2what runs underneath
facade.py
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class PatientFacade(Service):
    name = 'demo.facade.patient'

    def handle(self) -> 'None':

        # A FHIR-shaped request arrived over REST,
        # the answer comes from a legacy system
        mrn = self.request.payload['identifier']

        record = self.get_legacy_record(mrn)

        self.response.payload = {
            'resourceType': 'Patient',
            'identifier': [
                {'system': 'urn:mrn', 'value': mrn},
            ],
            'name': [{'family': record['family_name']}],
        }

What is a FHIR facade?

A FHIR facade is a service layer that exposes a FHIR-style REST API while translating each request into queries against systems that do not speak FHIR - HL7 v2 interfaces, databases, proprietary APIs - and translating the answers back into FHIR resources. Clients get the API they expect, and the legacy system is not modified, migrated or even aware the facade exists.

The facade holds no clinical data of its own - every answer is assembled from the source systems at request time, which is what distinguishes it from a FHIR server with a store behind it.

When should I build a facade instead of a full FHIR server?

Build a facade when the data already lives in systems that must remain the source of truth, when you need FHIR read access quickly, or when standing up and operating a clinical data repository is out of scope. The facade is a set of services - no data migration, no synchronization, no second copy of the record to govern.

Build or buy a full FHIR server when consumers need the full breadth of FHIR search semantics, history and versioning, profile validation, subscriptions, or persistence of resources that have no other home. A facade that keeps growing FHIR server features is a sign the project needs a full FHIR server.

Zato's role is the facade - it exposes REST APIs in front of anything, but it does not host or persist a FHIR server, does not validate against profiles and does not generate narrative. Both patterns also combine - a FHIR server for the resources you own, facades for the systems you wrap.

How do I build a FHIR facade in Python?

Legacy systems push their data as HL7 v2 feeds, so the facade is two services. The first receives the ADT feed over an MLLP channel and keeps a queryable copy of the demographics. The second answers FHIR-shaped reads over a REST channel from that copy, with the v2-to-FHIR field mapping following the mapping reference.

patient_facade.py
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class FeedIntake(Service):
    """ Receives the legacy ADT feed over an MLLP channel
    and keeps a queryable copy of the demographics.
    """
    name = 'demo.facade.feed-intake'

    def handle(self) -> 'None':

        # The channel already parsed the raw ER7 bytes ..
        msg = self.request.input

        # .. index the demographics by the MRN.
        mrn = msg.pid.patient_identifier_list.id_number

        self.cache.set(f'patient:{mrn}', {
            'family_name': msg.pid.patient_name.family_name,
            'given_name': msg.pid.patient_name.given_name,
            'date_of_birth': msg.pid.date_time_of_birth,
        })

class PatientRead(Service):
    """ Answers FHIR-shaped Patient reads, exposed through a REST channel
    as GET /facade/Patient?identifier=...
    """
    name = 'demo.facade.patient-read'

    def handle(self) -> 'None':

        # The MRN the FHIR client is asking about
        mrn = self.request.payload['identifier']

        # The copy maintained from the ADT feed
        record = self.cache.get(f'patient:{mrn}')

        date_of_birth = record['date_of_birth']
        birth_date = f'{date_of_birth[0:4]}-{date_of_birth[4:6]}-{date_of_birth[6:8]}'

        # Answer with a FHIR-shaped Patient
        self.response.payload = {
            'resourceType': 'Patient',
            'identifier': [{'system': 'urn:mrn', 'value': mrn}],
            'name': [{
                'family': record['family_name'],
                'given': [record['given_name']],
            }],
            'birthDate': birth_date,
        }

Where the legacy system must receive messages too - orders, updates - the facade writes back through an outgoing MLLP connection, with self.mllp['legacy-pas'].send(data) returning the acknowledgment result that says whether the legacy side accepted the message.

How do I keep the facade stateless and fast?

The facade itself stays stateless - every request contains what is needed to answer it, so instances scale horizontally and restarts lose nothing. Latency comes from the legacy side, and caching absorbs it - demographics that change rarely can be cached for minutes, with the scheduler refreshing hot entries in the background instead of making a client wait for a slow legacy round trip.

Acknowledgment handling matters too - the AckResult returned by outgoing MLLP calls says whether the legacy system accepted the query, so failures surface as proper HTTP errors instead of empty resources.

Frequently asked questions

Yes - that is what a facade provides. The limits are a matter of scope - a facade answers the reads and searches you implement, it is not a general-purpose FHIR endpoint with full search semantics, history or validation.

It is not a persistent store - no resource history, no versioning, no subscriptions, and search is only as capable as the legacy interface underneath. When consumers need those, put a full FHIR server behind the integration layer.

The REST channel has a security definition - Basic Auth, API keys or OAuth - like any other Zato REST API, independently of how the facade talks to the legacy systems behind it.

Facades provide FHIR access now, migrations remove the legacy system later. The two compose - the facade gives consumers a stable FHIR API, and when the legacy system is eventually replaced by a FHIR-native one, the facade's consumers do not notice.

Ready to put FHIR in front of your legacy systems?

Get started with Zato and expose your first FHIR-shaped endpoint in minutes.

Open source In Python