I’ve spent years building Rails applications, and I’ve found that truly understanding Ruby’s object model transforms how I write code. Let me share seven approaches that go beyond basic class definitions. These patterns help create applications that are easier to maintain, test, and extend over time.
Let’s start with a fundamental shift in how we think about objects. Instead of building complex inheritance hierarchies, I often compose objects from smaller, focused pieces. Think of it like building with Lego blocks rather than carving from a single piece of wood.
Here’s a practical example from an ordering system I worked on:
class OrderProcessor
def initialize(payment_gateway:, inventory_service:, notifier:)
@payment_gateway = payment_gateway
@inventory_service = inventory_service
@notifier = notifier
end
def process(order)
@payment_gateway.charge(order.total)
@inventory_service.reserve(order.items)
@notifier.send_confirmation(order)
order.complete!
end
end
When I test this, I can pass in simple test doubles instead of real services:
test_processor = OrderProcessor.new(
payment_gateway: FakeGateway.new,
inventory_service: MockInventory.new,
notifier: TestNotifier.new
)
This approach means each piece does one thing well. The payment gateway handles payments, the inventory service manages stock, and the notifier sends messages. They don’t need to know about each other. When credit card processing changes, I update the payment gateway without touching order logic.
Modules offer another way to add behavior to classes. I use them like toolkits that can be mixed into different classes. Here’s a validation module I created before I knew about Rails validators:
module Validatable
def self.included(base)
base.extend ClassMethods
base.class_eval do
before_validation :run_validations
end
end
module ClassMethods
def validation_rules
@validation_rules ||= []
end
def validates(field, options = {})
validation_rules << { field: field, options: options }
end
end
def run_validations
self.class.validation_rules.each do |rule|
validate_field(rule[:field], rule[:options])
end
end
private
def validate_field(field, options)
value = send(field)
if options[:presence] && value.nil?
errors.add(field, "can't be blank")
end
if options[:format] && value !~ options[:format]
errors.add(field, "has invalid format")
end
end
end
I can include this in any class that needs validation:
class User < ApplicationRecord
include Validatable
validates :email, presence: true, format: /@/
validates :age, numericality: { greater_than: 0 }
end
class Product < ApplicationRecord
include Validatable
validates :name, presence: true
validates :price, numericality: { greater_than: 0 }
end
The included hook runs when the module gets mixed into a class. It sets up the class methods and callbacks. I get reusable validation without duplicating code.
Sometimes I need behavior that applies to just one object, not all objects of that class. Ruby’s singleton classes make this possible. I used this for feature flags in a recent project:
class FeatureToggle
def initialize(name)
@name = name
@enabled = false
end
def enable_for(user)
user.define_singleton_method("#{@name}_enabled?") do
true
end
user.define_singleton_method("enable_#{@name}") do
# Enable feature for this user
end
end
def disable_for(user)
if user.respond_to?("#{@name}_enabled?")
user.singleton_class.send(:remove_method, "#{@name}_enabled?")
end
end
end
In a controller, it looks like this:
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
toggle = FeatureToggle.new('beta_dashboard')
if @user.beta_dashboard_enabled?
render 'beta_dashboard'
else
render 'standard_dashboard'
end
end
def enable_beta
@user = User.find(params[:id])
FeatureToggle.new('beta_dashboard').enable_for(@user)
redirect_to @user, notice: 'Beta features enabled'
end
end
The user gains a beta_dashboard_enabled? method that only exists on their instance. Other users don’t have this method. When I disable the feature, I remove the method. No database columns needed.
Delegation is another pattern I use frequently. It lets one object forward messages to another. Ruby’s Forwardable module makes this clean:
class UserPresenter
extend Forwardable
def_delegators :@user, :name, :email, :created_at
def_delegator :@user, :profile_picture, :avatar
def initialize(user)
@user = user
end
def display_name
"#{name} (#{email})"
end
def member_since
created_at.strftime("%B %Y")
end
def to_json
{
name: display_name,
avatar: avatar,
member_since: member_since
}
end
end
In a view or API endpoint:
def show
user = User.find(params[:id])
presenter = UserPresenter.new(user)
render json: presenter.to_json
end
The presenter delegates name, email, and created_at directly to the user object. It renames profile_picture to avatar for the presentation layer. The user model stays focused on business logic, while the presenter handles display concerns.
Ruby lets me define methods at runtime. I use this for configuration objects and dynamic interfaces:
class DynamicAttributes
def self.define_accessors(*attributes)
attributes.each do |attr|
define_method(attr) do
@data[attr.to_s] || @data[attr.to_sym]
end
define_method("#{attr}=") do |value|
@data[attr.to_s] = value
end
define_method("#{attr}?") do
!send(attr).nil?
end
end
end
def initialize(data = {})
@data = data
end
def to_h
@data.dup
end
end
I can create different configuration classes:
class UserPreferences < DynamicAttributes
define_accessors :theme, :notifications, :language, :timezone
end
class SystemSettings < DynamicAttributes
define_accessors :cache_timeout, :max_upload_size, :maintenance_mode
end
Using these objects feels natural:
prefs = UserPreferences.new
prefs.theme = 'dark'
prefs.notifications = true
prefs.language = 'en'
if prefs.notifications?
send_daily_digest
end
settings = SystemSettings.new
settings.maintenance_mode = true
if settings.maintenance_mode?
render_maintenance_page
end
The question mark methods follow Ruby conventions. They return true or false based on whether the value exists. This pattern works well when I don’t know all the attributes upfront.
Method chaining creates fluent interfaces that read like sentences. I use this for query builders and configuration:
class QueryBuilder
def initialize(model_class)
@model_class = model_class
@conditions = []
@includes = []
@order = nil
@limit = nil
@offset = nil
end
def where(condition)
@conditions << condition
self
end
def includes(association)
@includes << association
self
end
def order(field)
@order = field
self
end
def limit(number)
@limit = number
self
end
def offset(number)
@offset = number
self
end
def execute
query = @model_class.all
@conditions.each do |condition|
if condition.is_a?(Hash)
query = query.where(condition)
else
query = query.where(*condition)
end
end
@includes.each do |association|
query = query.includes(association)
end
query = query.order(@order) if @order
query = query.limit(@limit) if @limit
query = query.offset(@offset) if @offset
query
end
def to_sql
execute.to_sql
end
end
Building queries becomes readable:
recent_orders = QueryBuilder.new(Order)
.where("created_at > ?", 1.week.ago)
.where(status: ['processing', 'shipped'])
.includes(:customer, :items)
.order('created_at DESC')
.limit(50)
.execute
Each method returns self, allowing me to chain calls. The execute method collects all the conditions and builds the final query. I can see the SQL before execution with to_sql.
Module prepending changes how methods get called. It places prepended modules earlier in the method lookup chain:
module Logging
def save
Rails.logger.info "Saving #{self.class} #{id}"
result = super
Rails.logger.info "Saved #{self.class} #{id}"
result
end
end
module Timing
def save
start_time = Time.current
result = super
elapsed = Time.current - start_time
if elapsed > 1
Rails.logger.warn "Slow save: #{elapsed} seconds for #{self.class} #{id}"
end
result
end
end
module ValidationTracking
def save
if valid?
Rails.logger.info "Valid #{self.class} #{id}"
else
Rails.logger.warn "Invalid #{self.class} #{id}: #{errors.full_messages}"
end
super
end
end
class Document < ApplicationRecord
prepend ValidationTracking
prepend Timing
prepend Logging
def save
update_timestamps
# Business logic here
super
end
end
When I call document.save, the methods run in this order: Logging#save, Timing#save, ValidationTracking#save, Document#save, and finally ApplicationRecord#save. Each super calls the next method in the chain. I can add or remove concerns without changing the core save logic.
Sometimes I need an object that combines several other objects. Ruby’s method_missing helps create these composites:
class CompositeObject
def initialize(components)
@components = components
end
def method_missing(method_name, *args, &block)
@components.each do |component|
if component.respond_to?(method_name)
return component.send(method_name, *args, &block)
end
end
raise NoMethodError,
"undefined method '#{method_name}' for #{self.class}"
end
def respond_to_missing?(method_name, include_private = false)
@components.any? { |c| c.respond_to?(method_name, include_private) }
end
def components
@components.dup
end
end
I used this pattern for a user data object:
class UserData
def initialize(user_id)
@user = User.find(user_id)
@profile = Profile.find_by(user_id: user_id)
@preferences = Preferences.find_by(user_id: user_id)
@history = OrderHistory.new(user_id)
@composite = CompositeObject.new([
@user,
@profile,
@preferences,
@history
])
end
def method_missing(method, *args, &block)
if @composite.respond_to?(method)
@composite.send(method, *args, &block)
else
super
end
end
def respond_to_missing?(method, include_private = false)
@composite.respond_to?(method, include_private)
end
end
Now I can work with combined user data:
user_data = UserData.new(current_user.id)
puts user_data.name # From User
puts user_data.bio # From Profile
puts user_data.theme # From Preferences
puts user_data.recent_orders # From OrderHistory
user_data.update_theme('dark') # Calls Preferences#update_theme
The composite forwards methods to the first component that responds to them. respond_to_missing? tells other objects that our composite handles these methods. This creates a unified interface over multiple objects.
These patterns have served me well across different Rails applications. They help keep code organized as applications grow. Composition over inheritance makes testing easier. Modules create reusable behavior. Singleton methods allow instance-specific features. Delegation separates concerns. Dynamic methods adapt to changing requirements. Method chaining improves readability. Prepending modules layers functionality. Composites unify related objects.
The key is choosing the right tool for each situation. I start with simple composition, then add complexity only when needed. Ruby’s object model gives me these options without forcing me to use them everywhere. Each pattern solves specific problems I encounter while building maintainable Rails applications.