This blog post is the second in a three-part series detailing the evolution of Curai Health’s electronic health record (EHR) software, which powers our entire chat-based telehealth platform. This particular post talks about the technical details of our internal FHIR implementation.
Hi all, it’s Viggy, Jen2’s partner in Fast Health Interoperability Resources (FHIR)-related crime! In the previous post in this series, we’ve heard a lot about how we’re using FHIR to model data inside Curai’s EHR. However, FHIR is just a standard — there isn’t an official library that you can install to set FHIR to your application. The onus is on you, the developer, to implement something that is standards compliant.
At Curai, we have a Python-based backend and a React + JavaScript-based frontend. There are some well-regarded FHIR libraries for Java and .Net — but for Python, there is only the Smart on FHIR client, which helps to communicate with a FHIR server and serialize / deserialize FHIR models but doesn’t provide any sort of persistence layer. Given how tightly interwoven our EHR is with every facet of our application, we didn’t want to introduce a new server-side language to our stack just to host a FHIR server. Thus, we chose to roll our own implementation for FHIR data persistence.
We use a PostgreSQL database for most of our application data. In Python code, we interface with this DB using SQLAlchemy, one of the most widely-used and well-maintained ORMs in the world. We didn’t want to discard the ergonomics of using SQLAlchemy’s slick declarative API, and that formed the first requirement of our implementation: we wanted our FHIR data to be persisted into PostgreSQL tables and accessible via the ORM.
We also didn’t want to roll our own validation layer for actual FHIR resources. For this, we wanted to leverage the Smart on FHIR client library mentioned above, which supports the latest (R4) release of FHIR and has built-in validation. This formed another requirement of our solution: while the underlying implementation would likely be some sort of JSON column, we wanted to be able to work with Smart on FHIR client classes in our Python codebase for convenience’s sake.
Another issue that we wanted to address was one of contention. Our previous EHR implementation had no concept of version IDs, and so performed very poorly under contention. Put another way, if Alice and Bob both hold version 1 of a resource, and both try to write version 2, whoever writes last will “win.” This was both undesirable from an engineering perspective and a non-starter from a medical perspective. Whatever implementation path we chose, we wanted to make sure that contention was well handled.
Yet another requirement for us was what we refer to as “real-time chart updates.” In our system, there are many ways for charting information to be gathered, but one of the most common modalities is through specialized messages we present to the user in the chat stream. This is particularly useful for history taking and gaining context. One of the big drawbacks of traditional EHRs is the transcribing cost. If you ask the user if they have a headache, and they say yes, it is on the provider to enter “headache — present” (or a similar note) into the EHR. We wanted to eliminate this drawback entirely by allowing for updates in the chat stream to also update the chart — and, in the case of automation, vice versa.
A final constraint was performance. At scale, we wanted to be able to guarantee that we’d be able to pull up patient records efficiently. For this, we decided to rely on the JSONB column type that is native to PostgreSQLversions above 9.4. JSONB is a special column type that differs from JSON in one very important way: rather than storing data as encoded text, the DB instead stores binary blobs. This has one very important consequence: we are able to add indexes to JSONB data, even if it is deeply nested! This means we can do things like index on an Observation’s subject or encounter fields — which allows us to very efficiently look up observations for a particular patient or a particular encounter.
After doing a bunch of research, we arrived at a generic PostgreSQL schema derived from an open source library called FHIRBase. Each FHIR resource gets its own table with the following structure:
_id bigint primary key,_created datetime not null,_updated datetime not null,txid bigint not null,resource jsonb not null
We then map each table back to a SQLAlchemy ORM class. For example, here is our FHIR Patient class:
class Patient(Base): """Patient class. Based on https://www.hl7.org/fhir/patient.html. """ __tablename__ = "fhir_patient" txid = sa.Column(sa.BigInteger, nullable=False) resource = sa.Column(FHIRModel(patient.Patient), nullable=False)
The magic sauce here comes from the FHIRModel SQLAlchemy type decorator. This serializes Smart on FHIR classes to Python dictionaries when persisting to the DB, and deserializes from Python dictionaries back to Smart on FHIR classes when retrieving data from the DB.
So after all of this infra work, what do we get? Let’s say we wanted to retrieve a Patient with a particular ID and access their birthday. With the ORM class above, we can do so with just 2 lines of code:
patient = Patient.query.get(42)print(patient.resource.birthDate)
The next piece of our FHIR journey was to set up CRUD endpoints for creating, updating, and deleting FHIR resources. Again, the FHIR spec was there to save the day! A FHIR compliant server MUST implement the /fhirendpoint, which accepts Bundle resources and handles persisting the contents of the bundle to the database. By simply following the spec, we created a single endpoint that powers the entirety of our EHR from a persistence perspective.
We then tackled the real-time mechanics that were so fundamental to our use case. We already leveraged persistent chat connections via SocketIO for the exchange of messages between providers and end users. We extended this infrastructure to also include charting data in a separate namespace! Now, when a customer answers a question in the chat, the data is immediately persisted to FHIR and an update event is fired, thus updating the chart in real time.
The final missing piece was the opposite of storing data: how should we retrieve data for a particular encounter? Surprise surprise — again, the FHIR spec has written something about that! The /Encounter/<id>/$everythingendpoint is supposed to be used to fetch all FHIR data relevant to a particular encounter — which is literally exactly what we need! In our implementation, the endpoint simply aggregates all Observations, Conditions, Episodes of Care, and other pertinent resources and returns them in Bundle form to the client. Thus, the EHR workflow becomes:
One minor note: at the time we decided to FHIR-ify our EHR, we also looked at using FHIRBase off the shelf, using Google Cloud Platform’s FHIR data store, and storing the FHIR documents into a NoSQL document DB (like MongoDB). However, at the time, FHIRBase didn’t have Python bindings, GCP’s FHIR data store only supported an outdated version of FHIR, and as a company we had little-to-no experience handling Mongo-style NoSQLdatabases. As of now, FHIRBase has native Python bindings (although they aren’t ORM based) and Google Cloud Platform’s FHIR store supports FHIR R4. You may wish to use one of these solutions if you’d rather not roll your own!
The best part of this implementation is its flexibility. As Jen will detail in Curai’s next post, our initial FHIR data model had limitations, particularly when handling “snapshot in time” versus “current” data for long-running episodes of care. Rejiggering the data model to address these concerns required tweaks only in userland, without changing the underlying data layer. We’ve since added additional FHIR resources (and stored more information on existing resources) with nary an issue. Hooray!
To summarize, we found the following benefits and drawbacks of our original FHIR implementation:
Pros
Cons
Stay tuned for the next post in this scintillating series on Curai Health’s EHR evolution, where we solve many of these disadvantages via a Game of Thrones-inspired concept: mother of encounters.
Did you find this post interesting? Curai is actively hiring for multiple positions across many different disciplines. Check out our careers page here.