Most engineers will never write production MUMPS. I did — in VistA’s Pharmacy and Billing packages, running on Linux-based fis GT.M, at a scale that covered national-level healthcare. Every time I say this at a conference someone asks if I’m joking. I am not joking. This is what it actually looked like.
VistA is not an abstraction, it is a codebase
VistA (Veterans Health Information Systems and Technology Architecture) is the VA’s open-source EHR. It has been in continuous development since the 1970s. Its Pharmacy package is one of the oldest and most battle-tested drug-dispensing systems on the planet. When I say “battle-tested” I mean: it has survived 40+ years of VA programmers, budget cycles, MUMPS versions, hardware migrations, and Congress.
The codebase is not pretty. It is not designed. It is accreted —
layered, grown, patched, extended by people who are mostly dead or
retired. Routines have comment headers from 1987. You will find
; MODIFIED BY DPT 3/12/89 — DO NOT DELETE and you will absolutely not
delete whatever is below it, because you have no idea what it does and
neither does anyone still employed.
This is what production looks like when it has 40 years of uptime.
GT.M is MUMPS, but it ships on Linux and it is very fast
When people say “MUMPS,” they usually mean one of two runtimes: Caché (now IRIS) from InterSystems, or GT.M from FIS (formerly Greystone Technology, hence the G). GT.M is open-source, runs on Linux, and is what most VistA installations use. It is the runtime the VA uses. It is the runtime I used.
GT.M is — and I want to be careful here — legitimately impressive as a
storage engine. It implements MUMPS globals as a B-tree on disk with
journal-level crash safety. A SET ^PSDRUG(drugIen,"QTY")=qty is not a
round-trip to a database server. It is a write to a local B-tree that
GT.M will flush and journal. The process that executes the MUMPS routine
and the storage engine are the same process. There is no network hop.
There is no ORM. There is no query planner.
This sounds like a toy until you’re looking at dispensing-throughput benchmarks and wondering why a 1990s-era system on modest hardware outruns your shiny Spring Boot API on a write-heavy workload. Then it clicks: there is nothing between the application logic and the bits.
What writing Pharmacy routines actually looked like
The Pharmacy package manages drug inventory, dispensing orders, drug
interaction checks, and IV admixture records. In MUMPS. On ^PS*
globals (PS = Pharmacy System).
A dispensing routine looks roughly like:
DISPENSE(DFN,DRUG,QTY) ;dispense DRUG to patient DFN
N RESULT,AVAIL
L +^PSDRUG(DRUG,"STOCK"):5 E D ERRLK Q
S AVAIL=$G(^PSDRUG(DRUG,"QTY"))
I AVAIL<QTY D INSUF Q
S ^PSDRUG(DRUG,"QTY")=AVAIL-QTY
S ^PSDRUG(DRUG,"LAST")=$$NOW^XLFDT()
S RESULT="OK"
L -^PSDRUG(DRUG,"STOCK")
Q RESULT
Go ahead and stare at that. I’ll wait.
N is NEW (local variable scope). L +^GLOBAL:timeout is a lock
acquisition. $G() is GET with a default (never null-pointer errors,
MUMPS has $G). S is SET. Q is QUIT. $$ calls an extrinsic
function — $$NOW^XLFDT() calls the NOW label in the XLFDT routine,
which returns a timestamp in FileMan format (FileMan date is a whole
other post, do not get me started).
The global ^PSDRUG is hierarchical: the top-level subscript is the
drug internal entry number (IEN), and below it you have named sub-keys
like “QTY”, “STOCK”, “LAST”. This is the schema. There is no schema
file. The schema is implicit in the code. You learn it by reading code
and staring at globals with the GT.M D ^%G utility.
Drug interaction checking is a specific beast
The most anxiety-inducing code I wrote in VistA was related to drug
interaction checks. The ^PSSDI and ^PSDRUG globals hold interaction
data, and the Pharmacy package has routines that fire before a
dispensing order is confirmed to check whether the new drug interacts
with anything already in the patient’s active medication list.
What makes this stressful is not the MUMPS. The MUMPS is just syntax. What makes this stressful is that the logic is the safety net. There is no downstream service double-checking your work. The routine runs, the pharmacist sees the result, and if your interaction-check logic has a bug, a drug combination that should have been flagged gets dispensed.
I tested those routines obsessively. GT.M’s M-Unit (a testing framework that looks like JUnit if JUnit were designed in 1995) became my best friend. I set up scenarios with known interactions — warfarin and aspirin, methotrexate and NSAIDs — and ran them until the flags were consistent. This is probably the most careful I have ever been in my professional life about a piece of code.
The HL7 Messaging package is where things got weird
VistA’s HL7 package pre-dates HL7 v2 spec cleanup by years in some
cases. The routines that parse and generate HL7 messages are doing
string slicing in MUMPS — $E(MSG,start,end) to extract segments,
_ to concatenate, piece functions to split on delimiters.
What I found there: edge cases in patient demographic segments that had
been handled with ; KLUDGE — MO WILL FIX LATER comments from people
who clearly never came back to fix them. I fixed some. I left notes for
the rest. This is how legacy code survives — not by being cleaned up,
but by accumulating increasingly informative comments around the
load-bearing kludges.
The GT.M DB connector work at AFAQ
After my VistA time at EHS, I moved to AFAQ where we were building an EHR/EMR suite partly on top of VA VistA components. The gap I hit immediately: GT.M had no modern database connector. No JDBC. No REST adapter. No way for a Java app or a web frontend to talk to it without dropping into raw MUMPS or using an ancient TCP-based RPC layer that dated from the ’90s.
So I built new connectors. The challenge is that GT.M’s native access
mechanism is either in-process MUMPS or its $gtm_dist C API — a C
library that lets you call GT.M routines and access globals from C. We
wrapped that in a JNI layer so the Spring Boot backend could call it
directly. It was not beautiful. The error handling across the JNI
boundary was a particularly creative experience. But it worked, and it
cut round-trip latency by enough that the EHR’s chart-load time went
from “embarrassing” to “acceptable.”
The deeper insight from that project: GT.M is fast because it is simple. Every abstraction layer you add on top of it — REST, JNI, TCP RPC — costs you some of that simplicity. The trick is to add exactly enough abstraction for the consuming system to talk to it, and no more.
What I took away
Writing production MUMPS in a national-scale healthcare system is not something I recommend as a career path. I also would not trade it.
Here is what it taught me that modern backend work doesn’t:
- Storage costs are always present; most languages just hide them. In
MUMPS, every
S ^global(key)=valueis a storage operation. You never forget that writes cost something. In Java, you forget it constantly until your Hibernate session starts doing 400 queries per request. - Schema design is still design even when there’s no schema file. The hierarchy of your MUMPS global subscripts is your data model. Bad key choices cascade into bad access patterns that you can’t fix without rewriting everything that reads those globals.
- Crash safety matters more than you think. GT.M’s journaling means VistA installations run for years without data corruption events. My Postgres databases require careful transaction hygiene to achieve the same guarantee. The MUMPS model makes the safe path the default path.
I now write mostly Java and TypeScript. My databases are Postgres. My
runtimes have GCs. But when I review a schema design or argue about
transaction boundaries, some part of my brain is still in that GT.M
shell, staring at ^PSDRUG globals and thinking about what the reads
will look like at 3am during a dispensing surge.
That’s not nostalgia. That’s education.