A Guide to Rails Engines in the Wild: Real World Examples of Rails Engines in Action
Why are Rails Engines not used more often? I don’t know the answer, but I do think that the generalization of “Everything is an Engine” has hidden the problem domains that they can help to solve.
Why are Rails Engines not used more often? I don’t know the answer, but I do think that the generalization of “Everything is an Engine” has hidden the problem domains that they can help to solve.
A Certified Scrum Master and lead full-stack developer with over a decade of experience, Joe’s a Git pro, RoR veteran, and TDD enthusiast.
Expertise
Why are Rails Engines not used more often? I don’t know the answer, but I do think that the generalization of “Everything is an Engine” has hidden the problem domains that they can help to solve.
The superb Rails Guide documentation for getting started with Rails Engines references four popular examples of Rails Engine implementations: Forem, Devise, Spree, and RefineryCMS. These are fantastic real world use cases for Engines each using a different approach to integrating with a Rails application.
Examining parts of how these gems are configured and composed will give advanced Ruby on Rails developers valuable knowledge of what patterns or techniques are tried and tested in the wild, so when the time comes you can have a few extra options to evaluate against.
I do expect you to have a cursory familiarity of how an Engine works, so if you feel something is not quite adding up, please peruse the most excellent Rails Guide Getting Started With Engines.
Without further ado, let’s venture in the wild world of Rails engine examples!
Forem
An engine for Rails that aims to be the best little forum system ever
This gem follows the direction of the Rails Guide on Engines to the letter. It is a sizable example and perusing its repository will give you an idea of how far you can stretch the basic setup.
It is a single-Engine gem that uses a couple of techniques to integrate with the main application.
module ::Forem
class Engine < Rails::Engine
isolate_namespace Forem
# ...
config.to_prepare do
Decorators.register! Engine.root, Rails.root
end
# ...
end
end
The interesting part here is the Decorators.register!
class method, exposed by the Decorators gem. It encapsulates loading files that would not be included in the Rails autoloading process. You may remember that using explicit require
statements ruins auto-reloading in development mode, so this is a lifesaver! It will be clearer to use the example from the Guide to illustrate what is happening:
config.to_prepare do
Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
require_dependency(c)
end
end
Most of the magic for Forem’s configuration happens in the top main module definition of Forem
. This file relies on a user_class
variable being set in an initializer file:
Forem.user_class = "User"
You accomplish this using mattr_accessor
but it’s all in the Rails Guide so I won’t repeat that here. With this in place, Forem then decorates the user class with everything it needs to run its application:
module Forem
class << self
def decorate_user_class!
Forem.user_class.class_eval do
extend Forem::Autocomplete
include Forem::DefaultPermissions
has_many :forem_posts, :class_name => "Forem::Post", :foreign_key => "user_id"
# ...
def forem_moderate_posts?
Forem.moderate_first_post && !forem_approved_to_post?
end
alias_method :forem_needs_moderation?, :forem_moderate_posts?
# ...
Which turns out to be quite a lot! I’ve snipped out the majority but have left in an association definition as well as an instance method to show you the type of lines you can find in there.
Glimpsing the whole file might show you how manageable porting part of your application for re-use to an Engine might be.
Decorating is the name of the game in the default Engine usage. As an end user of the gem you can override model, view, and controllers by creating your own versions of the classes using file path and file-naming conventions laid out in the decorator gem README. There is a cost associated with this approach though, especially when the Engine gets a major version upgrade – the maintenance of keeping your decorations working can quickly get out of hand. I am not citing Forem here, I believe they are steadfast in keeping to a tight-knit core functionality, but keep this in mind if you create a Engine and decide to go for an overhaul.
Lets recap this one: this is the default Rails engine design pattern relying on end users decorating views, controllers and models, along with configuring basic settings via initialization files. This works well for very focussed and related functionality.
Devise
You will find an Engine is very similar to a Rails application, with views
, controllers
and models
directories. Devise is a good example of encapsulating an application and exposing a convenient integration point. Let’s run through how exactly that works.
You will recognize these lines of code if you have been a Rails developer for more than a few weeks:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
Each parameter passed to the devise
method represents a module within the Devise Engine. There are ten of these modules altogether that inherit from the familiar ActiveSupport::Concern
. These extend your User
class by invoking the devise
method within its scope.
Having this type of integration point is very flexible, you can add or remove any of these parameters to change the level of functionality you require the Engine to perform. It also means that you do not need to hardcode which model you would like to use within an initializer file, as suggested by the Rails Guide on Engines. In other words, this is not necessary:
Devise.user_model = 'User'
This abstraction also mean you can apply this to more than one user model within the same application (admin
and user
for instance), whereas the configuration file approach would leave you tied to a single model with authentication. This is not the biggest selling point, but it illustrates a different way to solve a problem.
Devise extends ActiveRecord::Base
with its own module that includes the devise
method definition:
# lib/devise/orm/active_record.rb
ActiveRecord::Base.extend Devise::Models
Any class inheriting from ActiveRecord::Base
will now have access to the class methods defined in Devise::Models
:
#lib/devise/models.rb
module Devise
module Models
# ...
def devise(*modules)
selected_modules = modules.map(&:to_sym).uniq
# ...
selected_modules.each do |m|
mod = Devise::Models.const_get(m.to_s.classify)
if mod.const_defined?("ClassMethods")
class_mod = mod.const_get("ClassMethods")
extend class_mod
# ...
end
include mod
end
end
# ...
end
end
( I’ve removed a lot of code (# ...
) to highlight the important parts. )
Paraphrasing the code, for each module name passed to the devise
method we are:
- loading the module we specified that lives under
Devise::Models
(Devise::Models.const_get(m.to_s.classify
) - extending the
User
class with theClassMethods
module if it has one - include the specified module (
include mod
) to add its instance methods to the class calling thedevise
method (User
)
If you wanted to create a module that could be loaded in this way you would need to make sure it followed the usual ActiveSupport::Concern
interface, but namespace it under the Devise:Models
as this is where we look to retrieve the constant:
module Devise
module Models
module Authenticatable
extend ActiveSupport::Concern
included do
# ...
end
module ClassMethods
# ...
end
end
end
end
Phew.
If you have used Rails’ Concerns before and experienced the re-usability they afford, then you can appreciate the niceties of this approach. In short, breaking up functionality in this way makes testing easier by being abstracted from an ActiveRecord
model, and has a lower overhead than the default pattern used by Forem when it comes to extending functionality.
This pattern consists of breaking up your functionality into Rails Concerns and exposing a configuration point to include or exclude these within a given scope. An Engine formed in this manner is convenient for the end user – a contributing factor to the success and popularity of Devise. And now you know how to do it too!
Spree
A complete open source e-commerce solution for Ruby on Rails
Spree went through a colossal effort to bring their monolithic application under control with a move to using Engines. The architecture design they are now rolling with is a “Spree” gem that contains many Engine gems.
These Engines create partitions in behaviour you may be used to seeing within a monolithic application or spread out across applications:
- spree_api (RESTful API)
- spree_frontend (User-facing components)
- spree_backend (Admin area)
- spree_cmd (Command-line tools)
- spree_core (Models & Mailers, the basic components of Spree that it can’t run without)
- spree_sample (Sample data)
The encompassing gem stitches these together leaving the developer with a choice in the level of functionality to require. For instance, you could run with just the spree_core
Engine and wrap your own interface around it.
The main Spree gem requires these engines:
# lib/spree.rb
require 'spree_core'
require 'spree_api'
require 'spree_backend'
require 'spree_frontend'
Each Engine then needs to customize its engine_name
and root
path (the latter usually pointing to the top level gem) and to configure themselves by hooking into the initialization process:
# api/lib/spree/api/engine.rb
require 'rails/engine'
module Spree
module Api
class Engine < Rails::Engine
isolate_namespace Spree
engine_name 'spree_api'
def self.root
@root ||= Pathname.new(File.expand_path('../../../../', __FILE__))
end
initializer "spree.environment", :before => :load_config_initializers do |app|
app.config.spree = Spree::Core::Environment.new
end
# ...
end
end
end
You may or may not recognize this initializer method: it is part of Railtie
and is a hook that gives you the opportunity to add or remove steps from the initialization of the Rails framework. Spree relies on this hook heavily to configure its complex environment for all of its engines.
Using the above example at runtime, you will have access to your settings by accessing the top level Rails
constant:
Rails.application.config.spree
With this Rails engine design pattern guide above we could call it a day, but Spree has a ton of amazing code, so let’s dive into how they utilize initialization to share configuration between Engines and the main Rails Application.
Spree has a complex preference system which it loads by adding a step into the initialization process:
# api/lib/spree/api/engine.rb
initializer "spree.environment", :before => :load_config_initializers do |app|
app.config.spree = Spree::Core::Environment.new
end
Here, we are attaching to app.config.spree
a new Spree::Core::Environment
instance. Within the rails application you will be able to access this via Rails.application.config.spree
from anywhere - models, controllers, views.
Moving on down, the Spree::Core::Environment
class we create looks like this:
module Spree
module Core
class Environment
attr_accessor :preferences
def initialize
@preferences = Spree::AppConfiguration.new
end
end
end
end
It exposes a :preferences
variable set to a new instance of the Spree::AppConfiguration
class, which in turn uses a preference
method defined in the Preferences::Configuration
class to set options with defaults for the general application configuration:
module Spree
class AppConfiguration < Preferences::Configuration
# Alphabetized to more easily lookup particular preferences
preference :address_requires_state, :boolean, default: true # should state/state_name be required
preference :admin_interface_logo, :string, default: 'logo/spree_50.png'
preference :admin_products_per_page, :integer, default: 10
preference :allow_checkout_on_gateway_error, :boolean, default: false
# ...
end
end
I won’t show the Preferences::Configuration
file because it’ll take a bit of explaining but essentially it is syntactic sugar for getting and setting preferences. (In truth, this is an oversimplification of its functionality, as the preference system will save other-than-default values for existing or new preferences in the database, for any ActiveRecord
class with a :preference
column - but you don’t need to know that.)
Here is one of those options in action:
module Spree
class Calculator < Spree::Base
def self.calculators
Rails.application.config.spree.calculators
end
# ...
end
end
Calculators control all sorts of things in Spree – shipping costs, promos, product price adjustments – so having a mechanism to swap them out in this manner increases the extensibility of the Engine.
One of the many ways you can override the default settings for these preferences is within an initializer in the main Rails Application:
# config/initializergs/spree.rb
Spree::Config do |config|
config.admin_interface_logo = 'company_logo.png'
end
If you have read through the RailsGuide on Engines, considered their design patterns or built an Engine yourself, you will know that it is trivial to expose a setter in an initializer file for someone to use. So you may be wondering, why all the fuss with the setup and preference system? Remember, the preference system solves a domain problem for Spree. Hooking into the initialization process and gaining access to the Rails framework could help you meet your requirements in a maintainable fashion.
This engine design pattern focuses on using the Rails framework as the constant between its many moving parts to store settings that do not (generally) change at runtime, but do change between application installations.
If you have ever tried to whitelabel a Rails application, you may be familiar with this preferences scenario, and have felt the pain of convoluted database “settings” tables within a long setup process for each new application. Now you know a different path is available and that’s awesome - high five!
RefineryCMS
Convention over configuration anyone? Rails Engines can definitely seem more like an exercise in configuration at times, but RefineryCMS remembers some of that Rails magic. This is the entire contents of it’s lib
directory:
# lib/refinerycms.rb
require 'refinery/all'
# lib/refinery/all.rb
%w(core authentication dashboard images resources pages).each do |extension|
require "refinerycms-#{extension}"
end
Wow. If you can’t tell by this, the Refinery team really knows what they’re doing. They roll with the concept of an extension
which is in essence another Engine. Like Spree it has an encompassing stitching gem, but only uses two stitches, and brings together a collection of Engines to deliver its full set of functionality.
Extensions are also created by users of the Engine, to create their own mash-up of CMS features for blogging, news, portfolio’s, testimonials, inquiries, etc. (it’s a long list), all hooking into the core RefineryCMS.
This design may get your attention for its modular approach, and Refinery is a great example of this Rails design pattern. “How does it work?” I hear you ask.
The core
engine maps out a few hooks for the other engines to use:
# core/lib/refinery/engine.rb
module Refinery
module Engine
def after_inclusion(&block)
if block && block.respond_to?(:call)
after_inclusion_procs << block
else
raise 'Anything added to be called after_inclusion must be callable (respond to #call).'
end
end
def before_inclusion(&block)
if block && block.respond_to?(:call)
before_inclusion_procs << block
else
raise 'Anything added to be called before_inclusion must be callable (respond to #call).'
end
end
private
def after_inclusion_procs
@@after_inclusion_procs ||= []
end
def before_inclusion_procs
@@before_inclusion_procs ||= []
end
end
end
As you can see the before_inclusion
and after_inclusion
just store of a list of procs that will be run later on. The Refinery inclusion process extends the currently loaded Rails applications with Refinery’s controllers and helpers. Here’s one in action:
# authentication/lib/refinery/authentication/engine.rb
before_inclusion do
[Refinery::AdminController, ::ApplicationController].each do |c|
Refinery.include_once(c, Refinery::AuthenticatedSystem)
end
end
I’m sure you have put authentication methods into your ApplicationController
and AdminController
before, this is a programatic way of doing it.
Looking at the rest of that Authentication Engine file will help us glean a few other key components:
module Refinery
module Authentication
class Engine < ::Rails::Engine
extend Refinery::Engine
isolate_namespace Refinery
engine_name :refinery_authentication
config.autoload_paths += %W( #{config.root}/lib )
initializer "register refinery_user plugin" do
Refinery::Plugin.register do |plugin|
plugin.pathname = root
plugin.name = 'refinery_users'
plugin.menu_match = %r{refinery/users$}
plugin.url = proc { Refinery::Core::Engine.routes.url_helpers.admin_users_path }
end
end
end
config.after_initialize do
Refinery.register_extension(Refinery::Authentication)
end
# ...
end
end
Under the hood, Refinery extensions use a Plugin
system. The initializer
step will look familiar from the Spree code analysis, here were are just meeting the register
methods requirements to be added to the list of Refinery::Plugins
that the core
extension keeps track of, and the Refinery.register_extension
just adds the module name to a list stored in a class accessor.
Here’s a shocker: the Refinery::Authentication
class is really a wrapper around Devise, with some customisation. So it’s turtles all the way down!
The extensions and plugins are concepts Refinery has developed to support their rich eco-system of mini-rails apps and tooling - think rake generate refinery:engine
. The design pattern here differs from Spree by imposing an additional API around the Rails Engine to assist in managing their composition.
“The Rails Way” idiom is at the core of Refinery, ever more present in the their mini-rails apps, but from the outside you wouldn’t know that. Designing boundaries at the application composition level is as important, possibly more so, than creating a clean API for your Classes and Modules used within your Rails Applications.
Wrapping code that you do not have direct control over is a common pattern, it is a foresight in reducing maintenance time for when that code changes, limiting the number of places you will need to make amendments to support upgrades. Applying this technique alongside partitioning functionality creates a flexible platform for composition, and here is a real world example sitting right under your nose - gotta love open source!
Conclusion
We have seen four approaches to designing Rails engine patterns by analyzing popular gems being used within real world applications. It is worth reading through their repositories to learn from a wealth of experience already applied and iterated upon. Stand on the shoulders of giants.
In this Rails guide, we have focused on the design patterns and techniques for integrating Rails Engines and their end users’ Rails applications, so that you can add the knowledge of these to your Rails tool belt.
I hope you have learned as much as I from reviewing this code and feel inspired to give Rails Engines a chance when they fit the bill. A huge thank you to the maintainers and contributors to the gems we reviewed. Great job people!