Pull to refresh

A Workable Proposal for Migrating a 20-Year-Old HIS to SMART on FHIR, One Strangler Fig at a Time

This is the English version of a full, executable migration proposal I wrote for a very concrete situation. We already hold three cards in hand: a legacy Hospital Information System that has run for more than 20 years, a FHIR Server we have already stood up, and the ability to build SMART on FHIR apps with Rails plus AI coding. The proposal argues against both extremes, do not replace the whole thing at once, and do not let old and new coexist forever. The middle path is the strangler-fig strategy, wrap a FHIR facade around the old HIS, grow every new feature on the FHIR side, let a standard interface take over the old functions one block at a time, and let the old HIS recede into a pure data source until it can finally be retired. It covers why gradual beats big-bang, the target layered architecture, a five-phase roadmap, the very first proof of concept you can start this week (exposing one read-only resource through a FHIR facade), PHI security and governance built in from step one, and the review discipline AI coding needs on critical medical systems (Spectra plus a criticality spectrum). The core claim is that the hard part of migration was never the technology, it is sunk investment, downtime risk, and vendor incentives, so every step of the route must be able to ship on its own, roll back on its own, and produce value on its own.

| 2,809 words | 8 min read | 12 views |

strangler-fig-infographic-en.jpg

This proposal answers a very concrete question. Inside our hospital we already have a legacy HIS that has run for more than 20 years, we have already stood up a FHIR Server, and we have the ability to build SMART on FHIR apps with Rails plus AI coding, so how do we move it over step by step from here? I am not talking about the slide-deck route where a brand-new system goes live next year. I am talking about an implementation route where every step can ship on its own, roll back on its own, and produce value on its own.

The conclusion first

Three things up front, everything after this is just the expansion:

  1. Do not replace the whole thing, and do not coexist forever either. A hospital cannot carry the risk of a full replacement, and endless coexistence means you never get to enjoy the benefits of standardization. The path in between is called the strangler fig, wrap a FHIR facade around the old HIS, grow every new feature on the FHIR side, let a standard interface take over the old functions one block at a time, and let the old HIS slowly recede into a pure data source, until one day only a few tables are still being read and it can be scheduled for retirement.
  2. The hard part of migration is not technology, it is risk and incentives. The old HIS cannot go down, cannot make mistakes, and its original vendor has no incentive to open it up. So every step of the route has to be designed as "add a block, do not touch the old one," do the read-only work first, then the writeback, and push the irreversible actions later and smaller.
  3. The three cards in our hand happen to add up to the minimum kit for a strangler fig. The FHIR Server is where the facade stands, Rails is the implementation skeleton for the facade and the translation layer, and AI coding plus Spectra discipline lets one person hold the review density a critical system requires.

Why gradual, and not a full replacement

I made the case in Taiwan's HIS Proprietary Lock-in that the cost of proprietary lock-in grows with time and the direction has to change eventually. But "the direction is right" is not the same as "flip it all at once." A big-bang replacement almost always fails in a hospital environment, for reasons that are very practical:

  • The cost of downtime is unbearable. Outpatient, inpatient, and emergency run 24 hours a day, and there is no weekend big enough to cut over 20 years of data and workflows in one shot without something breaking.
  • The hidden logic lives inside the old system. Twenty years of HIS accumulate more than data, they accumulate countless pieces of logic added on the fly for one department, one insurance rule, one accreditation cycle. None of it is documented, it lives only in the code and in the memory of longtime colleagues. Rewriting it all at once means rediscovering all of it at once, and the risk is exponential.
  • The verification surface is too large. Replace it all at once and you have to prove everything is correct at the same time. Cut it into small blocks and each time you only have to prove this one block is correct.

The spirit of the strangler fig is to let the old and new systems coexist for a period, but to have the new interface take over the old system's responsibilities one item at a time, the way a strangler fig wraps its host tree, so that once the new growth matures the old one is naturally hollowed out. This is the same thing I describe in Omakase Smart Hospital as "everything new grows the SMART on FHIR way, and the old proprietary parts slowly get wrapped in the standard." This proposal lands that idea as executable steps.

Target architecture: the old HIS becomes a data source, FHIR becomes the only new entry point

Draw the destination first, so you know which way each step is heading. The goal is not to "kill the old HIS," it is to demote it to one source system at the bottom of a layered architecture, standing shoulder to shoulder with the LIS and RIS/PACS (this layering is covered in TW Core IG Layered Architecture). Every new access, whether from an app, from AI, or across hospitals, goes through the FHIR interface on top, and no longer connects directly to the old HIS database.

flowchart TB
  subgraph NEWAPP[Every new feature grows on this layer]
    APP1[SMART on FHIR App<br/>OAuth2 / Scopes]
    CDS[Clinical Decision Support / CQL]
    XREF[Cross-hospital retrieval / BI]
  end
  subgraph FACADE[FHIR facade + translation layer, Rails]
    FS[FHIR Server<br/>TW Core Profile]
    ACL[Translation layer<br/>old HIS semantics to FHIR resources]
    POL[Policy Gateway<br/>authorization / audit / minimal disclosure]
  end
  subgraph SRC[Source systems, gradually pushed to the bottom]
    HIS[(20-year-old HIS<br/>read-only first, writeback later)]
    LIS[(LIS)]
    RIS[(RIS / PACS)]
  end
  APP1 --> POL
  CDS --> POL
  XREF --> POL
  POL --> FS
  FS <--> ACL
  ACL <-->|read: extract and map<br/>write: opened only later| HIS
  ACL --> LIS
  ACL --> RIS

Three key components:

  • FHIR Server: the unified entry and exit for data, wearing TW Core IG profiles, so both old and new data get accessed in the shape of standard resources. I evaluated how one person can operate it in Running a FHIR Server as a Kamal Accessory.
  • Translation layer (anti-corruption layer): this is the heart of the strangler fig. Written in Rails, it speaks the old HIS's private semantics on one side (the table structures and codes only the original vendor understands) and emits clean FHIR resources on the other. It keeps the "dirt" of the old system outside the facade, so the upper layers only ever see the standard.
  • Policy Gateway: authorization, audit, and minimal disclosure all concentrate in this one layer. This is exactly the Rails layer I split out in the FHIR Policy Gateway series, identity and authorization belong here, clinical logic belongs to CQL (see HAPI FHIR's Built-in CQL Engine), and keeping the two separate is what makes it auditable.

The five-phase roadmap

I cut the route into five phases, and each phase has the explicit property that "stopping here still has value." You do not have to walk all five phases to break even, each phase closes out on its own.

flowchart LR
  P0[Phase 0<br/>Inventory and foundation] --> P1[Phase 1<br/>Read-only facade]
  P1 --> P2[Phase 2<br/>First app]
  P2 --> P3[Phase 3<br/>Selective writeback]
  P3 --> P4[Phase 4<br/>Strangle and converge]

Phase 0: inventory and foundation (measure first, do not rush to write)

  • Resource inventory: list the data domains inside the old HIS that are the most valuable and relatively self-contained. Usually Patient (the master record), Encounter (visits), and Observation (labs and vital signs) are the best starting points, because they have the strongest read-only demand and the lowest write risk.
  • Mapping list: for each FHIR resource you want to expose, which old HIS table and fields it maps to, and how the codes convert (labs to LOINC, diagnoses to SNOMED CT, drugs to RxNorm). This mapping is itself the specification for the translation layer.
  • Foundation in place: FHIR Server mounted, TW Core profiles loaded, a Terminology service that answers queries (both CQL and validation depend on it, and a missing condition fails silently), Keycloak/OAuth2 stood up.

Value of stopping here: you now have a data map of the old HIS that quite possibly no one has ever assembled before.

Phase 1: read-only facade (read only, zero-risk start)

Pick one resource (I recommend starting from Patient or one class of Observation), write a read-only mapping in the translation layer, and let the FHIR Server answer standard queries like GET /Patient/{id} and GET /Observation?patient=&code=, returning data pulled live from the old HIS and converted into TW Core format.

  • Read-only is the key. This step never touches the old HIS's write path, the old system does not feel it, and if something goes wrong it is only a failed query, it will not corrupt any data. This is the lowest-risk point on the whole route and the best place for a first proof of concept.
  • The extraction method depends on how open the old HIS is: a read-only view or a replica is best, and if all you have is an existing interface (HL7 v2, file exchange) then do the conversion in the translation layer.

Value of stopping here: for the first time the hospital has a "speaks standard FHIR" patient and lab query interface that others can connect to.

Phase 2: the first SMART on FHIR app (let the value be seen)

With the read-only facade in place, use Rails plus AI coding to build a SMART app that people actually want to use, sitting on top of OAuth2 authorization. Pick a scenario that is "painful with the old HIS and easy with a standard app," for example a lab-trend view for one department, or patient-side record lookup (the approach from the Fireline Superman line).

  • The purpose of this step is political as much as technical: let the clinical side and management see with their own eyes that "you can grow new features without touching the old HIS," and win trust and resources for the later phases.
  • The app mounts through standard SMART, proving that "write once, portable later" is not a slogan.

Value of stopping here: a result that runs live and has users, instead of a slide deck.

Phase 3: selective writeback (open writes carefully, one at a time)

Only here do you start touching writes, and with extreme restraint. Pick a low-risk, simple-flow write scenario first, for example a class of note, a patient-reported outcome (PRO), or a kind of appointment. For data written in through a FHIR resource, the translation layer decides whether to write it back to the old HIS or keep it on the FHIR Server side, making that the new system of record for this data.

  • Open only one write path at a time, and for each one spell out clearly who owns this data going forward (the system of record): the old HIS or FHIR?
  • Writes are irreversible actions, so every step of this phase runs the full review discipline (see the next section), you do not "just vibe it in" because it is "only a small change."

Value of stopping here: the FHIR side starts becoming the system of record for some data, and for the first time a block of the old HIS's responsibility is genuinely taken over.

Phase 4: strangle and converge (let the old HIS recede until it can be retired)

As the system of record for more and more data domains moves to the FHIR side, the old HIS has fewer tables being read and narrower write paths, and it gets strangled from a "core system" down to "a few old tables still being read." Only then are the conditions right to discuss which old modules can be formally retired, which data goes into historical archive, and when the whole old HIS can be decommissioned.

Value of stopping here: this is what "replacing the old HIS" means in the traditional sense, but it is the accumulated result of every small step before it, not one big gamble.

The first proof of concept you can start this week

You do not have to wait for the whole roadmap to be approved. This week you can do one minimal slice of Phase 1:

  1. Pick one resource, I recommend Observation (one class of lab result, for example a common biochemistry item), because it is self-contained, read-only, and immediately meaningful to clinicians.
  2. Open a read-only source on the old HIS (a view, a replica, or an existing interface, pick one).
  3. Write a minimal translation layer in Rails: on receiving GET /Observation?patient=X&code=Y, fetch from the old HIS, convert to a TW Core Observation Laboratory Result FHIR JSON, and return it, with the code mapped to LOINC.
  4. Mount it behind the FHIR Server, and using one patient's real (de-identified) data, prove that a standard query returns the correct result.
  5. Wrap up: write the mapping list and code conversion for this one into a document, and it becomes the first specification for the translation layer as well as the template for the next resource.

One person can build this proof of concept within a week, and it never touches the old HIS's write path, so the worst-case failure is just "no result," it breaks nothing. It also validates three things at once: how to write the translation layer, how to apply the TW Core profiles, and the flow of Terminology mapping. Every resource you add after this just repeats the template.

PHI security and governance: built in from step one, not bolted on later

You are touching real medical records, so security cannot wait until later. A few principles to set from Phase 0:

  • All access goes through the Policy Gateway, no direct database connections. Authorization (who can read whom), audit (who read what and when), and minimal disclosure (return only the necessary fields) concentrate in this one layer, they do not scatter across every app.
  • OAuth2 / scopes for fine-grained authorization. SMART's scopes turn "this app can only read this patient's medications" into a standard way of writing it, instead of everyone hand-rolling their own.
  • De-identified data first. In the proof-of-concept and development phases use de-identified data across the board, and let real PHI flow only on an audited production path.
  • An auditable trail. Because identity authorization and clinical logic are layered apart (Policy Gateway versus CQL), when something goes wrong you can find out which gate let it through. This is exactly why I deliberately keep CQL away from authorization.

The review discipline for AI coding: the more critical, the heavier the human and the spec

For one person to maintain a system that touches medical records, AI coding is leverage, but it cannot be leverage out of control. My discipline for this is described in my full Agentic Workflow, and applied to this migration the key idea is the criticality spectrum: the same process, adjusting the density of human and spec according to risk.

  • Read-only facade (Phase 1): low risk, AI can generate fast, but it still runs Spectra's discuss/propose/apply/archive, putting the mapping spec under version control so someone else can pick it up later.
  • Writeback path (Phase 3): high risk and irreversible, so the human presses down hard. Every write scenario runs the full spec, runs spectra-audit to check for sharp edges, has tests, and has a rollback plan, and none of it gets skipped because "it is only a small change."
  • The common baseline: write intent into version-controlled artifacts and the agent will not drift, and you can review it afterward. What a medical system fears most is not writing slowly, it is vibe coding that "looks like it works but is quietly wrong" going live.

Kamal lets every phase deploy with zero downtime by one person, and roll back when needed, which turns "ship in small steps, roll back individually" from a slogan into something actually achievable.

Common concerns, which I do not dodge

  • "What if the old HIS vendor will not cooperate?" Phase 1 is read-only, and much of the time all you need to start is a read replica or an existing interface, without the vendor changing their system. That is the beauty of the strangler fig, you do not need the other side to open up on their own, you wrap a layer on the outside and start.
  • "What if the data mappings do not line up?" That is exactly why the translation layer exists. Misalignment is the norm, so handle it inside the translation layer and do not let it contaminate the upper-layer standard. Each resource's mapping is a spec you can refine over time.
  • "What if it stalls halfway?" It will, and that is fine. Every phase of this route is designed so that "stopping here still has value." Stop at Phase 1 and you have a standard query interface, stop at Phase 2 and you have a live app, you do not have to reach Phase 4 to break even.
  • "Can one person really carry this?" Carrying it depends on keeping the complexity of each segment down to something manageable: FHIR Server as a Kamal Accessory, the translation layer on Rails conventions, review with Spectra, deployment with Kamal. Every technical choice in this proposal was picked so that "one person can push it forward."
medical Public smart-on-fhir fhir his legacy-migration strangler-fig tw-core-ig hapi-fhir rails ai-coding spectra criticality-spectrum phi kamal oauth2 interoperability facade-pattern anti-corruption-layer one-person-framework 建議書