September 14, 2023

From microservices to a monolith using Ruby on Rails

Juan Pablo Tamez

Introduction

In recent years the technology landscape has seen microservices rise as a popular architecture choice for web applications. Teams of all sizes have adopted this trend with the purpose of developing independent services that interact between them rather than a single monolithic app containing all business logic. 

While there are some advantages that come from a microservice design, like logic encapsulation and the ability to scale services independently, the effort required to build and maintain such architectures is often underestimated.

Microservice-based designs are much better suited for large teams with

  • Multiple DevOps specialists, to handle coordinated deployments of each service. 
  • Ample resources, to have multiple dev teams and cover higher infrastructure bills. 
  • A proven market fit, as developing new features requires more planning than it would with a monolith. 

Small or medium teams building applications that are still maturing, constantly changing or trying to prove their market fit will often find that microservice structures are not only not ideal, but detrimental to their developing process. 

Building a sustainable microservice-based app often requires an amount of time and resources that smaller companies do not have. A web of interacting microservices means that a layer of inter-service communication must be developed, APIs are often chosen as the way to connect services between them. The added complexity of this layer of communication, as well as the infrastructure to support it, is easy to underestimate. This is sometimes enough to counter the simplification that is meant to come from loosely coupled services. Having many interacting services also often means, especially if the loosely coupled principle is not achieved, that time must be spent carefully planning changes that need to be implemented to multiple services at the same time. 

The monolithic approach, especially if built with a framework like Rails, offers some advantages that are much better suited for greater speed and agility. A Rails monolith allows for faster development since well-defined conventions decrease the number of decisions to make. Easier deployment process, there is only one deploy instead of many. And increased efficiency by having only one repository and a direct flow of data between the backend and frontend. This benefit is greater if HTML templates are used (along with Hotwire Turbo), instead of a single-page app technology like React, Vue, Angular, etc. 

Source: https://dev.to/alex_barashkov/microservices-vs-monolith-architecture-4l1m 

But what if you are already created a microservices application only to regret the decision? Are you looking for a better solution that will improve developers’ efficiency and a general simplification of the data flow?

While it is not an easy decision to make, in some cases, re-architecting the app back into a monolith may be your best choice. It will probably not be a simple process, however, it can also have some unexpected advantages:

    •    Is an opportunity to get rid of unnecessary code and features.

    •    Can be done gradually by embedding the new Rails pages using iframes or by first considering the new monolith as another microservice.

    •    Can be a turning point for creating better documentation, reconsidering some coding practices, etc. 

Our experience

At Telos, we recently had a client that was looking to re-architect their app from a distributed services approach. We led the migration of Node-based microservices into a new Rails monolith using Rails 6 “multiple databases” feature as the core of the initial development. We gradually connected over 10 databases to a single Rails app and were able to migrate core functionality into the new monolith. Part of the transition required using iframes in the old app that loaded pages hosted in Rails. The process was so successful that it effectively changed the client’s architecture direction and set up the grounds for building new features more efficiently. I’ll share with you a few important pointers on how we did it. 

The switch and how to do it

Database connection

One of the biggest challenges of trying to merge microservices into a monolith comes from how to deal with multiple databases. Traditional microservice architectures have distributed databases that each hold significant data tables. Fortunately, Rails recently added a new feature that can make the switching process much easier. Rails 6, and newer versions, allow you to connect different classes (models) to different DBs through simple configuration.

For example, assuming that a “posts_service_db” was the database dedicated to the “Posts service” and that we want to migrate that functionality into the “Post” model in the new Rails monolith. 

class Post < ApplicationRecord
self.abstract_class = true

 connects_to database: {
   writing: :posts_service_db,
   reading: :posts_service_db
 }
end

The “self.abstract_class = true” line defines that the “Post” model is not linked to an underlying table in the conventional Rails way. The “connects_to” clearly specifies which database the model is reading from and writing to.  

But maybe, the “posts_service_db” had other data tables that you want to map to different classes in the new monolith? This is easily doable, however, the Rails official guides suggest the following: 

“It's important to connect to your database in a single model and then inherit from that model for the tables rather than connect multiple individual models to the same database. Database clients have a limit to the number of open connections there can be and if you do this it will multiply the number of connections you have since Rails uses the model class name for the connection specification name.”

             - Rails Guides v 7.0.3.1

Let’s take a case where the “posts_service_db” also has a “comments” table, and we want to set that up as a separate model in Rails. As the Rails guides suggest, we should not have a “connect_to” to the same database in multiple classes. To resolve that we can create a new abstract class that will uniquely define connections to the “posts_service_db” and then inherit from that class in the models that need to. 

class AbstractPost < ApplicationRecord
 self.abstract_class = true

 connects_to database: {
   writing: :posts_service_db,
   reading: :posts_service_db
 }
end

class Post < AbstractPost
end

class Comment < AbstractPost
end

With the former setup, you now have two models that connect to one database. And the same principle can be replicated until each required table (model) is mapped to its corresponding database. 

Migrations

Configuring migrations to multiple databases is just as simple. You need to add a “migrations_path” value to the database section in the “database.yml” file. The migrations for each DB must also be placed in a folder prefixed with the database name. 

For example, migrations for the “posts” database would be in the “db/posts_migrate” folder, and the “database.yml” should look something like the following: 

development:
 posts:
   database: posts
   adapter: mysql2
   migrations_paths: db/posts_migrate

Being able to connect to multiple databases and add changes to them should give you the foundation to start migrating logic from distributed services into a monolith. 

As the migration develops there can be many nuances to the process depending on your specific needs. For example, new pages hosted by the monolith can be gradually introduced through iframes in the old app, or some services can be selectively kept and become available to the monolith through an API. 

Conclusion

Microservice-based architectures are not the best suited for newly born applications and quickly changing products. While switching to a Rails monolith is not a simple process, it may be the best choice for microservices that have become unscalable.

Using the “multiple databases” feature of recent versions of Rails will be of great use for making the switch, through simple configurations many databases can be mapped to corresponding models in Rails. 

For many new applications that have chosen the monolith way, some of Rails’ core principles like valuing integrated systems, convention over configuration, and optimizing for developers’ happiness may become essential in their path of building successful products. 

READY FOR
YOUR UPCOMING VENTURE?

We are.
Let's start a conversation.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Our latest
news & insights