Why (Not) Reverse Interop with Clojure

Written by J. David Smith
Published on 01 May 2021

One of the killer features of Clojure(Script) is that it is extremely easy to use code from the host ecosystem. While it has historically been annoyingly difficult to use npm packages with ClojureScript, Shadow CLJS has made that blessedly simple. This has been hugely beneficial—and not just in the abstact sense that having access to more code is obviously useful. I recently needed to add support for reporting COVID Vaccinations to our software, was able to simply import a Java library to get the job done. This is part of the Clojure sales pitch: it is a pragmatic language, for people that want to get work done.

In that same vein, I would call myself a pragmatic developer: my priority is getting work done, not necessarily in using the "perfect" tool or language to do it. However, that pragmatism extends to the post-deploy lifecycle of the software I write: it is not about building new features, it is about never getting panicked messages about broken websites or inaccurate reporting. As a dynamically typed language, I do not believe that Clojure is well-suited for my brand of pragmatism. Alas: it is what I am stuck with.

Or is it?

Clojure makes it easy to interoperate with code from the host language, but both the JVM and JavaScript ecosystems have multiple choices for language that can all co-exist in the same bundle. Why not build new features in one of those languages? One with things like type inference?

tl;dr - Map<Any, Any>. Clojure code uses a ton of plain-old map literals. Using and producing Clojure values from Scala or Java with the intent of processing the contents is frustrating and unwieldy. Using TypeScript or Elm on the frontend is still something I plan to explore due to their excellent support for this model (and the clj->js family of methods), but I haven't had a reason to do so yet.

The Task

This is the question that I found myself asking recently as I was faced with improving the reliability of one of our core systems: health insurance claim processing. Our software communicates with claim processors, submitting claims on behalf of pharmacists and handling the responses. This is a critical feature: if it doesn't work reliably, then nothing else about the software matters.

Unfortunately, the codebase I inherited was, ahem, under-tested. 118 lines of test code, as compared to over 20,000 lines of application code. Perhaps not the best metric, but enough to establish that test coverage was not good. This made it difficult to guarantee the reliability of this feature. Anything to do with health insurance is black magic as far as I'm concerned, so I'd been putting off dealing with the core code in the vain hope that I'd be able to effectively write a "safe" wrapper for the existing claim processing methods. It recently became obvious to me that this was not going to be possible—that the issues ran too deep, and I would need to dive in.

Given the critical importance of this subsystem, there were two options that I seriously entertained:

Writing tests is, of course, non-negotiable. However, it is easier to guarantee reliability in strongly-typed systems due to the ability to exclude large error classes, so the test suite for a Scala or Java implementation would (in theory) be smaller.