Why aren’t Rails Engines utilized more frequently? While I don’t have a definitive answer, I believe the notion that “Everything is an Engine” has overshadowed the specific issues they excel at resolving.
The excellent Rails Guide documentation for getting started with Rails Engines highlights four popular examples: Forem, Devise, Spree, and RefineryCMS. These practical implementations showcase distinct approaches to integrating Engines within a Rails application.

By examining how these gems are structured and configured, experienced Ruby on Rails developers can gain valuable insights into field-tested patterns and techniques, expanding their toolkit for future projects.
This exploration assumes a basic understanding of Engine functionality. For those seeking clarification, the comprehensive Rails Guide Getting Started With Engines provides a great resource.
Let’s delve into the world of Rails engine examples!
Forem
An engine for Rails that aims to be the best little forum system ever
This gem exemplifies the principles outlined in the Rails Guide on Engines. It’s a comprehensive example demonstrating the extensibility of the basic setup.
As a single-Engine gem, it employs a few techniques for seamless integration with the main application.
| |
A noteworthy aspect is the Decorators.register! class method provided by the Decorators gem. It handles loading files that fall outside Rails’ autoloading mechanism, circumventing the need for explicit require statements and preserving auto-reloading in development mode. Using the Guide’s example for illustration:
| |
Forem’s configuration primarily occurs within the main Forem module definition file, relying on a user_class variable defined in an initializer:
| |
While mattr_accessor facilitates this, it’s covered in the Rails Guide. Forem utilizes this setup to decorate the user class with the necessary components for its operation:
| |
The provided code snippet, while truncated, includes an association definition and an instance method to demonstrate the content found within.
Examining the complete file highlights the feasibility of extracting and reusing parts of your application as an Engine.
Decoration is central to the default Engine usage pattern. As a gem user, you can override models, views, and controllers by creating custom versions of classes based on file path and naming conventions outlined in the decorator gem’s README. However, this approach comes with maintenance overhead, particularly during major Engine upgrades. While Forem exemplifies a commitment to a focused core functionality, it’s crucial to consider the potential maintenance burden when significantly modifying your Engine.
In essence, this is the standard Rails engine design pattern: leveraging user-defined decorations for views, controllers, and models, complemented by basic settings configuration through initializer files. This approach excels in scenarios with well-defined and interconnected functionalities.
Devise
Rails Engines closely resemble Rails applications, featuring familiar directories like views, controllers, and models. Devise effectively encapsulates an application and offers a convenient integration point. Let’s explore its inner workings.
Seasoned Rails developers will recognize these lines:
| |
Each parameter passed to the devise method corresponds to a module within the Devise Engine. Ten such modules, inheriting from the familiar ActiveSupport::Concern, extend your User class upon invoking the devise method within its scope.
This integration approach offers flexibility, allowing you to tailor the Engine’s functionality by adding or removing parameters. Unlike the Rails Guide’s suggestion for Engines, it eliminates the need to hardcode the model within an initializer file. In other words, the following becomes unnecessary:
| |
This abstraction allows you to apply Devise to multiple user models within the same application (e.g., admin and user), surpassing the single-model limitation of the configuration file method. While not its primary advantage, it demonstrates an alternative problem-solving approach.
Devise extends ActiveRecord::Base with its own module, housing the devise method definition:
| |
This makes the class methods defined in Devise::Models accessible to any class inheriting from ActiveRecord::Base:
| |
(Extraneous code (# ...) has been removed for clarity.)
In essence, for each module name passed to the devise method:
- The specified module under
Devise::Modelsis loaded (Devise::Models.const_get(m.to_s.classify). - The
Userclass is extended with theClassMethodsmodule, if available. - The specified module (
include mod) is included to add its instance methods to the calling class (User).
Creating a compatible module would require adhering to the ActiveSupport::Concern interface while namespacing it under Devise::Models, the location from which the constant is retrieved:
| |
Rails developers familiar with Concerns and their reusability will appreciate the elegance of this approach. By decoupling functionality, testing becomes more manageable, and the overhead associated with extending functionality is reduced compared to Forem’s default pattern.
This pattern revolves around segmenting functionality into Rails Concerns and exposing a configuration point for their selective inclusion within a given scope. Devise’s success stems partly from its user-friendly Engine structure achieved through this approach. Now you too possess the knowledge to implement it.
Spree
A complete open source e-commerce solution for Ruby on Rails
Spree underwent a major transformation to manage its monolithic structure, transitioning to an Engine-based architecture. Their current design involves a “Spree” gem encompassing numerous Engine gems.
These Engines create distinct boundaries for functionalities typically found within a monolith or scattered across applications:
spree_api(RESTful API)spree_frontend(User-facing components)spree_backend(Admin area)spree_cmd(Command-line tools)spree_core(Models & Mailers, Spree’s essential components)spree_sample(Sample data)
This modular structure, orchestrated by the encompassing gem, empowers developers to choose the desired level of functionality. For instance, you could utilize only the spree_core Engine and build a custom interface around it.
The main Spree gem requires the following engines:
| |
Each Engine customizes its engine_name and root path (typically pointing to the top-level gem) while configuring itself by hooking into the initialization process:
| |
The initializer method, part of Railtie, provides a hook for modifying the Rails framework’s initialization steps. Spree leverages this extensively to configure the intricate environment for its engines.
Following the example, settings become accessible at runtime through the top-level Rails constant:
| |
This Rails engine design pattern guide alone provides valuable insights, but Spree’s codebase is a treasure trove. Let’s explore how they utilize initialization for shared configuration between Engines and the main Rails application.
Spree employs a sophisticated preference system, loaded by injecting a step into the initialization process:
| |
This attaches a new Spree::Core::Environment instance to app.config.spree, making it accessible throughout the application (models, controllers, views) via Rails.application.config.spree.
The created Spree::Core::Environment class resembles this:
| |
It exposes a :preferences variable, instantiated as Spree::AppConfiguration, which, in turn, utilizes the preference method from Preferences::Configuration to set options with defaults for general application configuration:
| |
While a detailed explanation of the Preferences::Configuration file is beyond the scope, it essentially provides syntactic sugar for managing preferences. This intricate system saves non-default values for new and existing preferences in the database for any ActiveRecord class with a :preference column.
Here’s an example of an option in action:
| |
Calculators govern various aspects of Spree (shipping costs, promotions, price adjustments), and the ability to swap them out highlights the Engine’s extensibility.
One way to override default preferences is within an initializer in the main Rails application:
| |
While the RailsGuide on Engines and Engine development experience might suggest the simplicity of exposing a setter in an initializer, Spree’s preference system addresses a specific domain problem. Leveraging the Rails framework through initialization hooks can facilitate maintainable solutions for your requirements.
This engine design pattern centers on using the Rails framework as a central repository for settings that typically remain static during runtime but vary across application installations.
Developers who have attempted to whitelabel a Rails application might recognize this preference scenario and the challenges posed by convoluted database “settings” tables and lengthy setup processes. Thankfully, this approach offers a compelling alternative.
RefineryCMS
RefineryCMS embraces convention over configuration, a welcome reminder amidst the potential configuration complexities of Rails Engines. The entirety of its lib directory is remarkably concise:
| |
This concise codebase speaks volumes about the Refinery team’s expertise. They introduce the concept of an “extension,” essentially another Engine. Like Spree, it features an encompassing gem, this time using only two stitches to integrate a collection of Engines for its full functionality.
Users can create their own extensions, composing a custom mix of CMS features for blogs, news, portfolios, testimonials, inquiries, and more, all seamlessly integrating with the RefineryCMS core.
Refinery’s modular approach is commendable. Let’s explore the mechanics.
The core engine defines hooks for other engines:
| |
The before_inclusion and after_inclusion hooks store a list of procs executed later. Refinery’s inclusion process extends the loaded Rails application with its controllers and helpers, as demonstrated here:
| |
This showcases a programmatic way of achieving what’s often done by placing authentication methods within ApplicationController and AdminController.
Examining the rest of the Authentication Engine file reveals more key components:
| |
Refinery extensions utilize a Plugin system. The initializer step mirrors Spree’s approach, satisfying the register method’s requirements for inclusion in the Refinery::Plugins list maintained by the core extension. Meanwhile, Refinery.register_extension simply adds the module name to a list stored in a class accessor.
Under the hood, Refinery::Authentication acts as a wrapper around Devise, illustrating the concept of “turtles all the way down.”
Extensions and plugins are Refinery’s solutions for managing its rich ecosystem of mini-rails apps and tooling, exemplified by rake generate refinery:engine. This design pattern sets itself apart from Spree by introducing an additional API layer around the Rails Engine to streamline their composition.
While Refinery embodies “The Rails Way,” particularly within its mini-rails apps, this remains hidden from the user’s perspective. Establishing clear boundaries at the application composition level is as crucial, if not more so, than crafting clean APIs for classes and modules within your Rails applications.
Wrapping external code is a common pattern, offering a proactive approach to minimizing maintenance efforts when that code undergoes changes. This technique, combined with functional partitioning, creates a flexible platform for composition. Refinery serves as a prime example of this approach.
Conclusion
We’ve explored four distinct approaches to designing Rails engine patterns by analyzing popular, real-world gems. Their repositories offer a wealth of knowledge and experience, encouraging us to learn from those who came before us.
This guide focused on design patterns and techniques for integrating Rails Engines with user applications, providing you with valuable tools for your development arsenal.
Hopefully, this code review has sparked your interest in exploring the potential of Rails Engines. A big thank you to the maintainers and contributors of the gems we examined.