26 Sep 2024
Once a Rails app has 50+ models and controllers, it’s time to organize the code by feature using Rails engines.
We give each feature a directory containing the app code, tests, migrations, and routes specific to it. It looks like a mini-rails app that only contains the code related to that feature.
Working with these mini-rails apps is a breeze! You can fully expand the app/*
directories, and they all neatly fit in the file explorer on the left-hand-side of your screen. It’s so satisfying to navigate such a small set of files.
I helped extract such engines on two large apps, and they were enthusiastically adopted. It doesn’t require much effort as long as you move files around without attempting to isolate the engine from the rest of the app.
aaa
enginesAt Zipline, we decided to name the directory that contains these engines aaa
. In a file browser, it looks like this:
- zipline-app/
- aaa/
- admin/
- checkout/
- forum/
- app/controllers/forum/
- posts_controller.rb
- comments_controller.rb
- app/models/forum/
- post.rb
- comment.rb
- config/routes.rb
- db/migrate/20240802_forum_posts.rb
- lib/
- test/
forum.gemspec
- surveys/
- app/
- controllers/
- models/
- 100+ models :cry:
- test/
Why aaa
? So that it sits at the top of the file browser, above the app
directory. 😅
I worked on an application where that directory was called engines
. While it makes more sense than aaa
, having it in the middle of the core application directories was messy—especially when you expanded a couple of core and engine directories. Now, while aaa
doesn’t stand for anything, you could argue that it’s an acronym for “Auxiliary Application Addons”. 🙃 “aaa engines” is now part of Zipline’s common language. It also helps us differentiate them from other kinds of engines (e.g. engines offering capabilities such as view components, form auto-save, etc).
We leverage Rails Engines to make Rails aware of app code, routes, and migrations stored in an aaa
subdirectory. We are NOT building configurable engines, isolated from the rest of the app, that get mounted at a specific path, like devise or active admin.
These simple aaa
engines only need three files to be picked-up by rails: a gemspec
, a lib/XXX.rb
, and an engine.rb
file. Here is the boilerplate code for an engine called forum
:
# aaa/forum/forum.gemspec
$:.push File.expand_path("lib", __dir__)
Gem::Specification.new do |spec|
spec.name = "forum"
spec.version = "1.0.0"
spec.authors = ["AAA"]
spec.summary = "The Forum AAA Engine"
end
# aaa/forum/lib/forum.rb
require "forum/engine"
module Forum
def self.table_name_prefix
"forum_"
end
end
# aaa/forum/lib/forum/engine.rb
module Forum
class Engine < ::Rails::Engine
end
end
Let’s make rails aware of the aaa
engine by adding a line to the Gemfile
:
# Gemfile
gem 'forum', path: 'aaa/forum'
Now that your engine is set up, you can move existing code from the core directories to the aaa
engine directory.
mkdir -p aaa/forum/app/models
mv app/models/forum aaa/forum/app/models/
Here is a script to make it a bit easier: extract-forum-to-aaa-engine.rb
Restart your rails app and it will just work - including code reloading in the development environment. ✌️
Models, controllers, and views don’t get automagically prefixed. So a model called Forum::Post
must be defined under aaa/forum/app/models/forum/post.rb
. This also allows you to define classes in multiple namespaces (or not namespaced).
# aaa/forum/app/models/forum/post.rb
class Forum::Post < ApplicationRecord
end
With the following lines added to your engine.rb
, you can store migrations under aaa/forum/db/migrations
.
module Forum
class Engine < ::Rails::Engine
initializer :append_migrations do |app|
append_engine_paths("db/migrate", app)
end
private
def append_engine_paths(type, app)
return if app.root.to_s.match root.to_s
config.paths[type].expanded.each do |expanded_path|
app.config.paths[type] << expanded_path
end
end
end
end
To generate a migration, you must run rails g migration ...
at the root level and then copy the migration from db/migrations
to aaa/forum/db/migrations
.
Routes defined in aaa/forum/config/routes.rb
will be loaded.
Rails.application.routes.draw do
namespace :forum do
resources :posts
resources :comments
end
end
You run tests from the root directory.
$> bin/rails test aaa/forum/test
You should update your CI config to run tests from aaa
engines alongside the core rails app tests.
If you use i18n-tasks
, you should register your engine paths to i18n-config.yml
.
If you use Sentry, I recommend adding stack.abs_path:**/aaa/** +app
to “STACK TRACE RULES” so that the stack trace from aaa
engines is considered part of the app and displayed by default.
It is that simple. We leverage Rails Engines to make Rails aware of the code that lives under aaa
sub-folders, and we enjoy working with smaller sets of files.
Some may want to isolate aaa
engines from the rest of the application by adding a public API. In my experience, that adds quite a bit of weight and complexity but can be a good fit once you reach Shopify’s scale (see Shopify’s Monolith - they use Rails engines to encapsulate components
and Packwerk to enforce isolation).
aaa
engines make it easier to experiment with new patterns or libraries (ViewComponent, event sourcing, etc) as the scope of the experiment can be limited to a specific aaa
engine.
I put together a sample rails app with an aaa
engine called Forum. Have a look on GitHub!
Give it a try, and let me know what you think!
Thanks to Rémi Mercier his feedback and comments.
You might also be interested in: