
March 25, 2025
Rails developers often face the challenge of writing clean, reusable, and maintainable code. Achieving this requires leveraging plugins, metaprogramming, and Ruby’s dynamic nature. In this article, we’ll explore practical ways to implement these techniques, ensuring your Rails code is DRY (Don’t Repeat Yourself) and efficient.
💡 Looking to improve your application’s code quality or build a scalable new project?
Whether you need cleaner, more efficient, and maintainable code or want to create a robust and scalable application from scratch, I’m here to help. Let’s take your project to the next level!
📩 Get in Touch1. Extracting Functionality into Plugins

One effective way to keep your Rails application clean is by extracting reusable functionality into plugins. A plugin is a self-contained package of code that can be shared across multiple applications or models. Let’s walk through the creation of a plugin using a Drivable example.
Step 1: Generating the Plugin
Rails provides generators to scaffold a plugin’s structure. Running the following command creates the necessary files:
./script/rails generate plugin drivable
This will generate a directory structure like this:
vendor/plugins/drivable/
├── init.rb
├── lib/
│ ├── drivable.rb
│ └── drivable/active_record_extensions.rb
├── Rakefile
├── README
└── test/
├── drivable_test.rb
└── test_helper.rb
- init.rb: Initializes the plugin.
- lib/drivable.rb: Hooks the plugin into Rails.
- lib/drivable/active_record_extensions.rb: Contains the actual logic for extending ActiveRecord.
Step 2: Defining the Behavior
We define behavior in lib/drivable/active_record_extensions.rb:
module Drivable
module ActiveRecordExtensions
module ClassMethods
def drivable
validates_presence_of :direction, :speed
include Drivable::ActiveRecordExtensions::InstanceMethods
end
end
module InstanceMethods
def turn(new_direction)
self.direction = new_direction
end
def brake
self.speed = 0
end
def accelerate
self.speed = [speed + 10, 100].min
end
end
end
end
- ClassMethods: Adds a drivable method to models.
- InstanceMethods: Defines turn, brake, and accelerate behaviors.
Step 3: Hooking into ActiveRecord
To integrate our plugin into ActiveRecord:
require "drivable/active_record_extensions" class ActiveRecord::Base extend Drivable::ActiveRecordExtensions::ClassMethods end
Finally, we bootstrap the plugin in init.rb:
require File.join(File.dirname(__FILE__), "lib", "drivable")
Now, any model can use drivable behavior:
class Car < ActiveRecord::Base drivable end
By using plugins, we ensure modular, reusable, and easy-to-maintain code.
2. Using Metaprogramming to Write DRY Code
Metaprogramming helps reduce duplication and create flexible, reusable code.
Problem: Repetitive Status Methods
Consider a Purchase model with multiple statuses:
class Purchase < ActiveRecord::Base
def self.all_in_progress
where(status: "in_progress")
end
def self.all_submitted
where(status: "submitted")
end
def in_progress?
status == "in_progress"
end
def submitted?
status == "submitted"
end
end
Adding a new status requires updating multiple methods, leading to duplication.
Solution: Metaprogramming
We can dynamically generate these methods:
class Purchase < ActiveRecord::Base
STATUSES = %w(in_progress submitted approved shipped received)
validates :status, presence: true, inclusion: { in: STATUSES }
class << self
STATUSES.each do |status_name|
define_method "all_#{status_name}" do
where(status: status_name)
end
end
end
STATUSES.each do |status_name|
define_method "#{status_name}?" do
status == status_name
end
end
end
This:
- Centralizes statuses in STATUSES.
- Dynamically generates finder and accessor methods, reducing duplication.
Improving Readability with Macros
To further improve maintainability, we can extract this logic into a macro:
class ActiveRecord::Base
def self.has_statuses(*status_names)
validates :status, presence: true, inclusion: { in: status_names }
status_names.each do |status_name|
scope "all_#{status_name}", -> { where(status: status_name) }
end
status_names.each do |status_name|
define_method "#{status_name}?" do
status == status_name
end
end
end
end
Now, the Purchase model is simplified:
class Purchase < ActiveRecord::Base has_statuses :in_progress, :submitted, :approved, :shipped, :received end
This abstraction improves readability and makes the feature reusable across models.
3. Benefits of DRY Code
Writing DRY code offers several advantages:
- Maintainability: Changes are localized.
- Readability: Common patterns are abstracted away.
- Reusability: Code can be shared across projects.
For example, to add a virtual status not_shipped, we can now do:
class Purchase < ActiveRecord::Base
has_statuses :in_progress, :submitted, :approved, :partially_shipped, :fully_shipped
scope :all_not_shipped, -> { where.not(status: ["partially_shipped", "fully_shipped"]) }
def not_shipped?
!(partially_shipped? || fully_shipped?)
end
end
Exceptional cases like not_shipped now stand out clearly.
4. Plugins vs. Gems

While plugins are useful for internal reuse, gems are better suited for distributing code across teams or the open-source community. Gems offer features like versioning and compatibility with non-Rails projects.
For example, the drivable plugin could be converted into a gem for broader use.
Conclusion
By leveraging plugins, metaprogramming, and DRY principles, you can write cleaner, more maintainable Rails applications. These techniques reduce duplication while improving readability and reusability. Whether you’re building an internal tool or an open-source library, these practices will help you create robust and scalable solutions.
Final Thought: Don’t fear metaprogramming—it’s a powerful tool that, when used wisely, can elevate your Ruby and Rails development skills to the next level.
