This post is a shout out to the amazing features of Rails that allow developers to extract code fairly easily from monolith application into stand-alone modules. It aims to point out the advantages of Rails engines over microservices and acknowledges the cons of engines solution.
I’m a back-end developer, and I got hooked up on Rails over 4.5 years ago. Since then, I’ve spent almost three years as a professional working on a single product initially developed in 2011. This is my first post, and I hope you like it.
A few weeks ago, I started researching ways to improve the architecture of my company’s main application, which is fairly old and fairly big (81,980 commits and counting). Over time, many developers have tried to keep it in the best possible shape. Despite going beyond the standard MVC model and adding layers to the application, it is no longer sufficient. We have encountered typical problems related to big monolith systems: very large cognitive load required to maintain the mental model of a system, slow test suite, dead code, etc.
In such a situation, the first idea that pops out is to transition such a system into a shiny microservice world. As tempting it sounds, such as transition is not always easy or even possible for a number of reasons:
- It moves application codebase complexity into the system infrastructure architecture
- It introduces new kinds of problems, mostly with communication, synchronization, and integration.
- It requires a significant amount of base work to prepare the system for such change before even a single business module can be moved out of a monolith
- It makes the local development environment setup more complex
Teams choosing a microservices approach should have a number of skilled software architects along with dev ops specialists, and a fair amount of time, to accomplish such goal.
As for my case, microservices are off the table. I have continued looking, and I have noticed a convenient middle way, as you will read on . . . .
Engines are similar to a standard Rails application but in miniaturized form. Their structure is very similar to a typical Rails project; one can find inside all familiar MVC parts of Rails architecture, such as models, views, controllers, jobs, and channels. There are a few minor differences, but what is most important, and which makes them such a useful tool, is that instead of being run as separate applications, Rails engines are mounted inside larger systems. This feature also implements a convenient mechanism that prevents name collisions between host application and engine code, while still allowing for cross-referencing when needed.
With that in mind, one of the coolest use cases of engines is the ability to extract code grouped in namespaces inside monolith host application into engines that are mounted in the parent application. This solution delivers most of the promises of microservices, such as system modularity, reduced cognitive load required to work on a single service and decreased execution time of test suite.
Moreover, Rails engines shine in a few particular areas:
- Because they are mounted inside the host application, they are not affected by communication and integration issues related to independent microservices.
- There is an easy setup; in fact, Rails engines could be initiated with a single Rails command.
- The smoothness of extracting bits of code, as an engine, at first could be merely a subdirectory of a host application. Files could be moved between them using drag and drop.
- The ability to be extracted to a stand-alone repository, in the form of a gem. And be reused by the number of applications.
- Different versions could be included in different apps, based on the used gem version number, which makes developing new features and A/B testing simpler.
Because there is no silver bullet in software development, this approach also has its cons:
- The host application, besides being composed with many independent modules, is still a single system that could be affected by dependencies conflicts among multiple engines.
- Rails engines do not offer the technological freedom of microservices.
- The host application still requires a significant amount of hardware resources, as a monolith would.
- Despite its convenience, the ability to cross-reference the host application and the mounted engine also makes it easier to produce unwanted coupling between them.
Based on the above analysis, the pros outweigh the cons by far, particularly for smaller developer teams that do not have many resources to perform a transition to microservices. Even in terms of such transition, Rails engines could be used as a middle step, allowing a series of smaller changes rather than one big leap. Each previously extracted engine could be mounted later on in a smaller Rails app and used as microservices itself. What also should be mentioned is that engines are well ‘battle tested’ as some very popular solutions adopt them (e.g. devise and spree gems),
I hope you’ve enjoyed this post. In my next one, I’ll provide a tutorial for ‘Hello World’ in the Rails engines world.