Guides

From plugins to gems and engines: how Rails extensions evolved

Guides · · Published 2026-10-24

A black and red steam locomotive pulling carriages out of a country station
A restored steam locomotive leaving a country station.

If you learned Rails before 2012, "plugin" meant a folder of Ruby code copied into vendor/plugins. Today the word survives mostly in the name of a generator. Here is how the change happened, and why it still matters when you maintain an older application.

The vendor/plugins years

In Rails 1.x and 2.x, an extension was usually installed with script/plugin install followed by a Subversion or Git URL. The command copied the plugin's source into vendor/plugins/<name>, and Rails loaded every directory there at boot, running each plugin's init.rb. There was no version resolution: what you had was whatever revision happened to be checked out on the day you ran the command.

This model had real advantages. You could read and patch the code in place, and installing a plugin did not require any system-wide setup. Its drawbacks grew with the ecosystem, though:

Meanwhile, the same code was often also published as a gem on RubyGems, and declared in config/environment.rb with config.gem. Many projects shipped both forms, which is why old README files often show two installation methods.

Rails 2.3: plugins grow up

Rails 2.3 (2009) gave plugins the ability to carry their own app/ directory, with controllers, helpers, views and routes, borrowing ideas from the separate "Rails Engines" project. For the first time, a plugin could ship a small, self-contained piece of an application rather than just extending Active Record. The Rails 2.3 release notes describe the change.

Rails 3: Bundler, Railties and engines

Rails 3.0, released in August 2010 after the merge with Merb, rebuilt the framework around modular components. Two changes transformed extensions:

  1. Bundler became the standard way to declare dependencies. A Gemfile lists the gems, Bundler resolves compatible versions and records them in Gemfile.lock, and every developer and server gets the same set.
  2. Railties gave gems an official hook into the boot process. A gem defines a subclass of Rails::Railtie to add initializers, configuration options, rake tasks or generators, without any file in vendor/plugins. Rails::Engine, itself a Railtie, goes further and lets a gem behave like a miniature application.

A minimal Railtie looks like this:

module MyGem
  class Railtie < Rails::Railtie
    config.my_gem = ActiveSupport::OrderedOptions.new

    initializer "my_gem.configure" do |app|
      ActiveSupport.on_load(:active_record) do
        include MyGem::Model
      end
    end
  end
end

The ActiveSupport.on_load hook is worth noting: instead of requiring Active Record early and patching it, the gem waits until the framework has loaded the class. Gems that ignore this hook are a common cause of slow boots and odd initialisation-order bugs, even today.

Rails 3.1 added the rails plugin new generator, with --full and --mountable options and isolate_namespace to keep an engine's models, routes and helpers separate from the host application. Rails 3.2 deprecated vendor/plugins, and Rails 4.0 removed support for it. From then on, every Rails extension has been a gem.

Plugins, Railties and engines today

The current Rails guide on plugins still uses the word "plugin", but it means a gem generated with rails plugin new. In practice you will meet three shapes:

ShapeWhat it doesExamples
Plain gemRuby code with no Rails hooks; you call it explicitly.image_processing, Pagy's core
Railtie gemHooks into boot: configuration, initializers, generators, rake tasks.lograge, which registers its configuration and log subscribers through a Railtie
EngineShips models, controllers, views, routes or assets; often mounted at a path.Devise, ActiveAdmin, GoodJob's dashboard, Mission Control – Jobs

The Getting Started with Engines guide explains mountable engines in detail. Rails itself is now a set of engines and Railties: Active Storage, Action Text and Action Mailbox are all engines that ship with the framework.

Why this history matters

Old tutorials, blog posts and Stack Overflow answers are still widely read, and many of them assume the plugin era. A few practical consequences:

Writing your own extension

If you need to share code between applications, start with rails plugin new my_extension, which creates a gem skeleton with a dummy application for tests. Add --mountable only if you need routes, controllers or views. Keep the surface area small, use ActiveSupport.on_load rather than requiring framework classes directly, and declare the range of Rails versions you support in the gemspec. Those habits are exactly what we look for when evaluating a gem.