Rails Monolith towards Engines spike - Our story

Tute Costa September 25, 2020

This year my team started working on a new UI for our patient check-in application, with slightly modified rules for specific use cases and forms. It would reuse most of our backend software with few changes. We named it Bariloche.

Bariloche is a new UI for our patient check-in workflows, targeted for Urgent Care facilities. Given that the UI would start from scratch, we’d use it as a testbed to build our frontend entirely with React Components. Everything would be namespaced under the “Bariloche” name.

Such a clear boundary between the monolith and this product made it an ideal case to try an Engine implementation. Given Bariloche would share the data layer declared by the main app and not need database migrations, our first step towards Engines would be smaller than for other potential extractions we could foresee.

Why an Engine?

An Engine architecture would enable the Bariloche team to run specs in a greenfield app without the need to run the entire suite frequently. A Rails Engine depends on the host app and not the other way around: changes in Bariloche couldn’t break the parent app. Running fewer tests would accelerate feedback and development while decreasing our CI bills.

git diff mounting an Engine instead of declaring routes

An explicit declaration of dependencies within the app would let us see component boundaries, dependency directions, and circular dependencies more clearly, encouraging us to be more mindful when referencing classes. I noticed a few anomalies when trying to disentangle Bariloche out of the monolith and found they couldn’t have happened in an Engines architecture.

The question behind the spike: Is it worth it for us?

I didn’t know whether we were “big enough” that it’d be worth it, and the spike would shed some light on our costs and benefits equation.

My hunch was that it might be early for the change: we were ten developers, rarely blocking each other, running a healthy test suite, and delivering features at a pace that felt good for us. If it ain’t broke don’t fix it, right? But I also thought that it’s better to untangle earlier rather than later if the team deems the cost to be worth it.

Moreover, if we liked it better, we could extract other parts of the app and further optimize our test suite runtimes, decreasing the surprise factor of code living in an isolated Engine outside the app/ directory only for this project.

Some implementation details

The spike, by definition, was short-lived. During those experiments, we discovered the following.

With Bariloche being a Rails application on its own right, names can’t clash with the parent app, and for example, bariloche_completed_path became just completed_path, the namespace declaring the product context of every file.

Class name clashes couldn’t happen either. The need for the :: prefix in some class references and related bugs disappeared. An example again with the CompletedStep object, that exists without prefix as a model that tracks patient’s progress, and prefixed by Bariloche, Previsit, and NavigationList for the other contexts we have. (I know, either the model or the navigation steps should be renamed, Monday-morning quarterbacks!)

I wanted to have spec/features/bariloche specs defined within the Engine but didn’t manage to do it before concluding I wouldn’t move forward with this approach.

Conclusion for our team

I started the spike seeking to accelerate our velocity with:

  • Better code organization
  • More explicit dependency declarations
  • Inclusion only of relevant parts of our codebase for developing and testing components
  • Faster spec runs and more rapid feedback on changes

We could get those benefits with an unusual architecture for us. I was one of the few developers thinking about changing how we organize our code; the change wouldn’t be natural at first for everyone.

Feature specs would naturally live in the Engine to exercise the new UI, but the shared models’ unit specs would live in the main Rails application (part of the goal was to share most backend code that was already running).

If we moved forward, Engine specific migrations (for Bariloche or another future Engine) seemed like a problem to us: we’d either have to use the main app’s DB schema or we’d have to keep them in sync. I didn’t have a satisfactory answer yet.

Finally, for faster spec runs during Bariloche development, we could run the following command: bundle exec rspec spec/**/bariloche/*_spec.rb.

So I closed the PR and shared what I learned with the team. The one monolith continued to be the best solution for us.

Related links: