Photo by Joshua Fuller on Unsplash

Rails Modular API with Engines

Andre Nunes
Runtime Revolution
Published in
8 min readSep 26, 2023

--

In this blog post, we will explore the world of Ruby on Rails APIs and discuss how to build modular APIs using Rails Engines. We will cover the basics of defining a Ruby on Rails API, the challenges of a monolithic API, the concept of Rails Engines, and how we can leverage Rails Engines to enhance our Ruby on Rails API monolith.

At Runtime Revolution, we have been developing Ruby on Rails applications for quite some time and as the demand for robust and scalable APIs continues to rise, we recognise the importance of making the optimal application for our use case.

In our pursuit of top-notch solutions, we continuously explore innovative approaches to optimise the performance and efficiency of our Ruby on Rails applications. We will share our insights on defining a Ruby on Rails API, highlight the challenges inherent in monolithic API architectures, introduce the concept of Rails Engines, and highlight how they can revolutionise the way we enhance our Ruby on Rails API monolith.

By the end of this article, you will have a comprehensive understanding of how to leverage Rails Engines to create highly modular and scalable APIs in your Ruby on Rails applications.

How to define a Ruby on Rails API

A Ruby on Rails API is a type of web application that is specifically designed to expose data and functionality to be consumed by other applications or services. The API follows the principles of Representational State Transfer (REST), where resources are exposed as endpoints with specific URLs. These endpoints support standard HTTP methods (such as GET, POST, PUT, DELETE) to perform operations on the resources. Usually, the API responds to requests with data formatted in JSON (JavaScript Object Notation), a commonly used format for exchanging data between systems.

Using Ruby on Rails to build a RESTful API with JSON responses, we can create a robust and interoperable system for exchanging data and functionality between different applications and services.

So what is the problem with a Rails Monolith?

Having all in one place is great, but when the application starts growing it can be a pain to add new features, refactor old code, fix some issue and make sure that the application continues to work as before.

Complexity: Large monolithic Rails applications tend to become more complex over time. As the codebase expands, it becomes harder to understand and maintain. Complex interdependencies between different parts of the application can make it challenging to introduce changes or fix issues without unintended consequences. Complexity can also make it difficult to onboard new team members and make it difficult to navigate and reason about the codebase.

Scalability: Monolithic applications can face scalability challenges. Scaling the entire application becomes more difficult as it grows in size, and it may require scaling components that don’t necessarily need it. As a result, scaling can be inefficient and costly. Additionally, a monolithic architecture limits the ability to independently scale different parts of the application, such as separating high-traffic components from low-traffic ones.

Team Collaboration: Collaboration can become challenging in a large monolithic Rails app. With many developers working on different features simultaneously, conflicts and coordination issues may arise. Teams might need to coordinate closely to avoid stepping on each other’s toes or causing conflicts in the codebase. Communication and coordination become crucial to ensure efficient collaboration.

Testing: Large monolithic applications can have extensive test suites, and running the entire test suite can be time-consuming. As the application grows, the number of tests increases, and test execution time can become a bottleneck. Longer test runs slow down development cycles and can lead to less frequent testing or skipping certain tests altogether, potentially compromising code quality and reliability.

Continuous Integration and Deployment: Deploying a large monolithic Rails application can be time-consuming and prone to errors. Each code change often requires rebuilding and deploying the entire application, resulting in longer deployment times. This can impede rapid iteration and continuous integration practices. Furthermore, a single bug or issue in a particular feature can impact the entire application, causing downtime or regression issues.

Technology Stack Limitations: In a monolithic Rails application, the technology stack is typically shared across the entire application. This limits the ability to adopt new technologies or frameworks for specific features or components. It can also inhibit experimentation and innovation, as making changes to the technology stack or introducing new tools can require significant effort and impact the entire application.

Dependency Management: Managing dependencies within a monolithic application can be challenging. With a large codebase, keeping track of dependencies and ensuring compatibility becomes more complex. Updating dependencies or introducing new ones can have far-reaching effects, potentially introducing conflicts or breaking existing functionality.

We have a couple of options to mitigate these problems, adopting a microservices architecture or consider modularization techniques like breaking the monolith into smaller, more manageable components using Rails Engines. For this post we are going to take a look at Rails Engines and how we can use them to enhance our Rails application.

But before jumping into Rails Engines let me explain a concept that will help us to break our Rails app into more specific components.

Domain-Driven Design

Domain-Driven Design (DDD) is an approach to software development that focuses on understanding and modelling the domain of a problem to create software solutions that closely align with the business requirements. DDD emphasises collaboration between domain experts and developers to build a shared understanding of the problem domain and to create a domain model that represents the key concepts and relationships within that domain.

To apply Domain-Driven Design (DDD) principles in a Rails application, first we need to identify the core domain. This means determining the most important business value and understanding the problem domain by collaborating with domain experts (or product owners).

After the core domain is well determined we need to establish a shared language between domain experts and developers to accurately represent domain concepts.

This step is crucial to create the model of the domain because the domain experts and the development team have different backgrounds and it’s important that we are both on the same page. The model domain is a structured domain model using object-oriented principles, reflecting the core concepts and relationships.

After that we can start applying these concepts to our application by dividing the Rails application into smaller, manageable components using bounded contexts, encapsulating domain logic in order to ensure consistency and enforce business rules.

When all the components are well defined and are validated with tests to ensure the expected behaviour of each individual component we can then integrate them in the main Rails App. When all the components are integrated we can create integration tests on the main Rails app to ensure that all functionalities work as expected together.

By following these steps, we can apply DDD principles to your Rails application, resulting in a well-structured and maintainable codebase that accurately represents the problem domain and aligns with business requirements.

To apply DDD principles we can use the Packwerk gem developed by Shopify. It helps promote a more modular and maintainable codebase by enforcing architectural boundaries and providing a clear understanding of dependencies. It encourages clean code organisation and can be a valuable tool for large and complex Rails applications.

Rails Engines

Rails Engines are a powerful feature in the Ruby on Rails framework that allow to package and share functionality as a modular, self-contained component within a larger Rails application. Think of an engine as a mini-application that can be embedded and mounted within a parent Rails application.

Rails Engines provide a way to extract reusable code, such as models, controllers, views, routes, assets, and encapsulate them into a separate and isolated component. This modularization helps organise and structure our codebase, making it easier to manage and maintain.

Engines have their own directory structure, similar to a Rails application, and they can define their own routes, models, views, migrations, and assets. This separation allows to develop and test engines independently of the main application, promoting clean architecture, code reusability and facilitates collaboration among teams working on different parts of the application.

Rails Engines are particularly useful when we want to create modular components or isolate some functionality in order to break down a monolithic Rails application into smaller and manageable pieces.

Once mounted on the main app, the engine’s routes and functionality become accessible within the parent application. With this in mind we can use the Domain-Driven Design approach and apply it with Engines.

Example

Let’s create a rails application for a Book Library, applying the Domain-Driven Design and move our models to an Engine called Repository.

In this Repository Engine we will have a Book model and the controller for CRUD operations for this model.

rails new library-api --api

rails plugin new repository -- mountable --dummy_path spec/dummy

cd repository

rails g model Book title:string author:string genre:string

We can them include this Engine in our main application. We are using a monorepo to store our code but we can also have the engine in a different repo and include it on the Gemfile.

# Gemfile

...
# Engines
gem 'repository', path: './engines/repository'

Then we can add the routes of the engine in our app routes.

# routes.rb

Rails.application.routes.draw do
mount Repository::Engine => '/'
end

And it’s done! We separated the domain from our main app. This is a simple example but with a more complex scenario we can have multiple domains that interact in our main application separated in different engines.

A Rails Engine can be a piece of code that adds functionality to our application, it does not need to have models or routes associated. That why it is important to identify the domains of our business, split them into different engines and create a shared domain if needed.

Conclusion

In this blog post, we explored the process of defining a Ruby on Rails API and discussed the challenges associated with a monolithic API architecture. Then introduced Rails Engines as a solution to enhance the modularity and maintainability of a Ruby on Rails API monolith. By using Rails Engines, we can extract and encapsulate reusable functionality, making the API more modular, scalable, and easier to maintain. With a well-designed modular API architecture, we can build robust and flexible APIs that can adapt and grow with changing business requirements.

--

--