From plugins to gems and engines: how Rails extensions evolved

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:
- No dependency management. If two plugins needed different versions of a library, nothing warned you.
- No reliable upgrades. Updating a plugin meant deleting the folder and reinstalling, often losing local patches.
- Load-order surprises. Plugins were loaded alphabetically unless you configured
config.plugins, and many monkey-patched Rails internals that changed between minor releases.
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:
- Bundler became the standard way to declare dependencies. A
Gemfilelists the gems, Bundler resolves compatible versions and records them inGemfile.lock, and every developer and server gets the same set. - Railties gave gems an official hook into the boot process. A gem defines a subclass of
Rails::Railtieto add initializers, configuration options, rake tasks or generators, without any file invendor/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:
| Shape | What it does | Examples |
|---|---|---|
| Plain gem | Ruby code with no Rails hooks; you call it explicitly. | image_processing, Pagy's core |
| Railtie gem | Hooks into boot: configuration, initializers, generators, rake tasks. | lograge, which registers its configuration and log subscribers through a Railtie |
| Engine | Ships 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:
- Installation instructions. A command such as
script/plugin install git://github.com/...will not work on any supported Rails version. Look for the gem name on RubyGems.org instead, and check that it has been released since the tutorial was written. - Abandoned forks. When a plugin's author did not move to Rails 3, users often published forks with names such as
something-rails3. Those forks are usually as abandoned as the original by now. The plugin histories section lists the common cases. - Upgrading legacy apps. An application still on Rails 2.3 or 3.x may have code in
vendor/pluginsthat nobody remembers adding. Before upgrading, inventory it: some of it will be published gems you can declare in the Gemfile, some will be local patches you need to turn into initializers or files underlib/, and some will be features now built into Rails. - Features absorbed by Rails. Several famous plugins became unnecessary because Rails grew the feature: file attachments with Active Storage, background jobs with Active Job and Solid Queue, and password authentication with the Rails 8 authentication generator.
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.
GuidesEvaluating a Rails gem
A practical checklist for judging a Ruby gem: maintenance signals, compatibility with Rails 7 and 8, dependencies, licence, and security advisories with bundler-audit.
Plugin historiesPaperclip
What Paperclip did, why thoughtbot deprecated it in 2018, the kt-paperclip fork, and how to migrate Paperclip attachments to Active Storage or Shrine.
Plugin historiesSpork
What Spork did for RSpec and Cucumber in the Rails 2 and 3 era, why it stopped at 1.0.0rc4, and how Spring, Bootsnap and parallel testing solve the same problem today.