Editor’s note: this post comes from our engineering team, for those interested in a technical view under the hood.

Background and Motivation

Most software engineers are intensely passionate (some might say rigid) about specific philosophies behind building and organizing software. These debates range from spaces vs. tabs to microservices vs. monoliths and everything in between. 

Here at Kindbody the product and engineering team is as passionate as any, but our philosophy is more along the lines of strong opinions, loosely held. We care more about making things work than following the one true path. This has served us well so far and allowed us to stay open minded and flexible until we find a solution that works for us.

Our biggest sources of developer angst so far have been addressing lackadaisical app performance and figuring out the best way to move quickly building out new features in our customized KindEMR. We kicked around several ideas for addressing these issues and ultimately decided to re-organize some of our medium-sized apps into one monorepo. This is the path that led us there.

Issues and Incremental Improvements

The KindEMR consists of several apps with different responsibilities from scheduling appointments to seamlessly ordering labs and prescriptions for patients. Our initial setup for these apps followed a traditional pattern with the apps communicating via REST API calls. Unfortunately, the apps were built in such a way that several API calls might be happening within one controller action in order to assemble the data for a view. 

As our clinical staff grew and usage of the apps increased, we quickly saw performance issues with memory bloat and latency. To address these issues we decided to replace some of those API calls with read only connections among the app databases. This not only improved the performance of our apps but also reduced development time as we built out new features of the EMR.

Inevitable Downsides

The downside to this rapid development was an increasing maintenance headache. We were creating copies of the models across our apps which meant not only duplicate model code but also code divergence between apps for the same model. By our calculations, the worst case scenario might include nearly 16K lines of duplicate code. We debated options for avoiding this, including custom gems and Rails engines, but we were most intrigued by the possibility of building out a monorepo that would allow us to share model code across apps.

Building the Monorepo

Most of our current apps are built in Rails. Advantages of Rails include its ubiquity in the startup community and the large amount of online resources available. Unfortunately, there is not much advice out there on building a Rails monorepo so I was initially skeptical about how much effort it would require and whether it would be worth it. Spoiler alert: it was worth it!

Our monorepo consists of the three apps in our EMR that are most used and have the most tightly coupled data. The entire process of getting our three apps into a monorepo and deployed to production took about one week of developer time. We were able to spread this out over several weeks so that we could continue with regular app maintenance and development as we made the transition.

We started off by combining two of our smaller apps. Initially, we focused on making only one of these apps production ready. Each of these apps took about a day a piece (we started doing monorepo Fridays). It was really only our last and largest app that took us several consecutive days to get up and running. Some preparation time also went into each of the apps before we attempted to combine and deploy. This mostly consisted of ensuring that the gems and initializers related to our models were consistent across the apps we were combining.

Once our initial app prep was done the next step was to mash the two apps together and ensure that our one set of models contained all of the methods used by both apps. We organized our code so that the monorepo consisted at the top level of two app folders and one model folder with nested folders for each of the app models.

Something like this:

|-- app1
|-- app2
|-- models
|   |-- app1
|   |-- app2

This set up works for us because our deployment processes are fully Dockerized. So our steps for building and deploying each app in Jenkins look something like this:

cp -R ./models ./app1/app/
cd ./app1
docker build -t $IMAGE_TAG .

This allows us to do minimal customization for our production apps. Something like this:

config.eager_load_paths += %W( #{config.root}/models/app1 #{config.root}/models/app2} )
config.autoload_paths << ('./app/models')
config.autoload_paths << ('./app/models/app1')
config.autoload_paths << ('./app/models/app2')

Even this customization isn’t strictly necessary. However, we liked the idea of maintaining some organization to help our team keep track of which models belonged to which database. This way we didn’t require namespacing when we used these models in the codebase.

Lastly we needed some custom configuration for our development environment:

config.autoload_paths << ('../models')
config.autoload_paths << ('../models/app1)
config.autoload_paths << ('../models/app2')

Voila! Monorepo. 

The relatively small amount of time we invested in setting up the monorepo has proved worthy in both app performance and developer happiness. Our business logic is now consistent across apps with one set of models and we can continue to leverage the speed and ease of read only database connections without duplication of code. Setting up the monorepo and minimizing duplication of code has reduced our maintenance headaches, made it easier to track down potential bugs and has helped us respond quickly to requests for new features in the KindEMR.

Sam Yunker
Sam Yunker