Back-end12 minute read

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.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

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.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Joe James
Director

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.

Share

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.

Every Rails guide should cover the topic of Rails engines design patterns and their examples.

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

A flexible authentication solution for Rails

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 the ClassMethods module if it has one
  • include the specified module (include mod) to add its instance methods to the class calling the devise 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

An open source content management system for Rails

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!

Hire a Toptal expert on this topic.
Hire Now

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.