Maintainable Rails: A Rails Engine Strategy
If you’ve ever worked for a company or organization that runs multiple Rails applications, you’ve most likely seen these projects using different sets of plugins, front-end frameworks, coding techniques, test frameworks, etc. etc. Ultimately having a ton of technical debt leading to heavy context switching slowing progress to a crawl. Most of the time it’s inevitable because keeping multiple applications up-to-date and consistent is a daunting task.
I’ve seen this countless times in my career when running a consulting company and when I worked for a big IT company. In this article, I want to share with you one technique I use to help pay down the technical debt and pave a path forward to a more sound development team using Rails Engines.
Quick intro to Rails Engines
The term “engine” can be confusing because it’s not really anything different than what we’ve already been working with. A Rails Engine is a Rails application with some special options that allow us to “hook” into it. I think of it as a Rails module (like a Ruby module) that we’re adding to our code, but instead of getting additional Ruby methods, we can a whole web application at our disposal.
Let’s kicks things off with an example engine. Let’s say our company wants to build an engine called
$ rails plugin new company_stack --mountable
We’re going to isolate our engine so that we can namespace our ruby code to avoid any collisions in
naming. This will help our engine be independent from any application that uses it. It’s important
to think of our engine as completely separate from any application that uses it. Combined with the
--mountable option (above), using the
isolate_namespace will provide independence we need:
# lib/company_stack/engine.rb module CompanyStack class Engine < ::Rails::Engine isolate_namespace CompanyStack end end
Now we can include our engine in our Rails application’s Gemfile:
gem "company_stack", path: "../path/to/company_stack"
Let’s say our company has a few Rails applications in production. Upper management is complaining about the amount of time it takes to ship code. Some of our new engineers aren’t as familiar with older versions of Rails so it’s more difficult to assign team members to projects. Our senior engineers are complaining about the sheer amount of redundant code across our projects. Our Devops team wants to upgrade our servers, but can’t because some applications can’t run on newer versions of Ruby, SSL, Postgres, etc etc. Sound familiar?
What are our goals with making our development team more efficient?
- Reduce the overall development churn so we can ship new features faster.
- Provide a clear direction on our company’s official technical stack.
Using Rails Engines to Define Our Organization Stack
Do we know our company’s official technology stack? We hope that it’s Ruby, but what about
the gems and libraries we should be using? This is crucial in keeping our applications
consistent. How many times have we come across a project that uses Rspec and another one using
Minitest? It’s important to define what and how we should be writing our applications so
that we can keep our code consistent and reduce the context switching. A good place to start is the
gemspec file in our engine:
# /company_stack.gemspec Gem::Specification.new do |spec| spec.add_dependency 'rails', '>= 22.214.171.124' spec.add_dependency 'turbo-rails', '~> 1.0.1' spec.add_dependency 'hotwire-rails', '~> 0.1.3' spec.add_dependency 'pg', '>= 1.3.5' spec.add_dependency 'view_component', '~> 2.53.0' spec.add_dependency 'redis', '~> 4.8' spec.add_dependency 'cssbundling-rails', '~> 1.1.1' spec.add_dependency 'jsbundling-rails', '~> 1.0.2' spec.add_dependency 'bcrypt', '~> 3.1.18' spec.add_dependency 'puma, '~> 5.6' ... end
Here we are defining that Rails 7 with Hotwire, Postgres, Redis, and Puma as our official stack. We
can include additional gems like Mintest, Devise, Sidekiq - you name it. This is taken from my own
company’s official stack and it means all Rails applications that includes the
officially approved. If your company’s Rails applications are older, then it most likely makes sense
require lesser versions of Rails, say 6 and up until everything is caught up.
Central Place for Shared and Common Code
Once we have our engine installed on our Rails applications, we can begin to explore ways to reduce
code redundancy. Of course, each application is unique, but there will always be cases of common
code. For example, most applications has a
User model and that can be one area to extract common
Here’s a simplified example: We’ve looked at a few of our Rails applications and we’ve noticed that we’re authenticating the User in multiple ways. As an organization, it would be nice that all our projects authenticate the same way so we know that our applications are secure and up-to-date. We’ll create a Concern in our engine that authenticates a user using email and password.
# /app/models/concern/company_stack/users/auth module CompanyStack module Users::Utils extend ActiveSupport::Concern class_methods do def login(login, password) ... end end end end
Now, all we have to do is include the Concern in our Rails application and we instantly have a way to authenticate user records. Eventually, we can remove the old code from the application and just use the engine to authenticate our users.
class User < ApplicationRecord include CompanyStack::Users::Authentication include CompanyStack::Users::Utils .. end #=> current_user = User.login("mattsears", "******")
This is a very simplified example, but hopefully it illustrates the potential in using Rails engines
as a way to cut down technical debt. The nice thing about using Concerns, is that our applications
aren’t required to use the new authentication until they’re ready i.e. including
CompanyStack::Users::Authentication in our User model. The additional bonus is we don’t have to
rewrite the user authentication for any new applications we create.
And now our Rails application includes all of our company’s official front-end stack and is ready to take advantage of all the code that has already been written - keeping our Rails applications not only using less code, but looking consistent too.
We don’t have to stop here either, we can include application helpers, background workers, view components, and more in our engine.
Hopefully I’ve illustrated the power as using with Rails Engines as an overall strategy to help your organization get on the right track with more maintainable applications. For my company, we’ve been using this strategy for a while and it has paid a lot of dividends and I think it can help you’re team too.