Rails Development

Essential Ruby Patterns Every Rails Developer Should Know

Ruby patterns are reusable solutions to common programming problems. Understanding these patterns will make your Rails code more maintainable, readable, and efficient. Let's explore the most important patterns for Rails developers.

Object-Oriented Patterns

1. Service Objects

Service objects encapsulate business logic that doesn't naturally fit in models or controllers.

# app/services/user_registration_service.rb
class UserRegistrationService
  attr_reader :user, :errors

  def initialize(user_params)
    @user_params = user_params
    @errors = []
  end

  def call
    create_user
    send_welcome_email if user.persisted?
    create_default_preferences if user.persisted?

    user.persisted?
  end

  private

  attr_reader :user_params

  def create_user
    @user = User.new(user_params)

    unless @user.save
      @errors.concat(@user.errors.full_messages)
    end
  end

  def send_welcome_email
    UserMailer.welcome_email(@user).deliver_later
  rescue => e
    @errors << "Failed to send welcome email: #{e.message}"
  end

  def create_default_preferences
    @user.create_preference(
      theme: 'light',
      email_notifications: true
    )
  end
end

Usage in controller:
```ruby
class UsersController < ApplicationController
def create
service = UserRegistrationService.new(user_params)

if service.call
redirect_to service.user, notice: 'Welcome!'
else
@user = service.user
@errors = service.errors
render :new
end
end
end
```

2. Form Objects

Form objects handle complex forms that span multiple models.

# app/forms/order_form.rb
class OrderForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :customer_name, :string
  attribute :customer_email, :string
  attribute :product_id, :integer
  attribute :quantity, :integer
  attribute :delivery_date, :date

  validates :customer_name, :customer_email, presence: true
  validates :product_id, :quantity, presence: true, numericality: { greater_than: 0 }
  validates :customer_email, format: { with: URI::MailTo::EMAIL_REGEXP }

  def save
    return false unless valid?

    ActiveRecord::Base.transaction do
      create_customer
      create_order
      update_inventory
    end

    true
  rescue => e
    errors.add(:base, "Failed to create order: #{e.message}")
    false
  end

  def customer
    @customer
  end

  def order
    @order
  end

  private

  def create_customer
    @customer = Customer.find_or_create_by(email: customer_email) do |c|
      c.name = customer_name
    end
  end

  def create_order
    @order = @customer.orders.create!(
      product_id: product_id,
      quantity: quantity,
      delivery_date: delivery_date
    )
  end

  def update_inventory
    product = Product.find(product_id)
    product.decrement!(:stock_quantity, quantity)
  end
end

3. Query Objects

Query objects encapsulate complex database queries.

# app/queries/user_search_query.rb
class UserSearchQuery
  def initialize(relation = User.all)
    @relation = relation
  end

  def search(params = {})
    @relation = filter_by_name(params[:name]) if params[:name].present?
    @relation = filter_by_email(params[:email]) if params[:email].present?
    @relation = filter_by_role(params[:role]) if params[:role].present?
    @relation = filter_by_status(params[:status]) if params[:status].present?
    @relation = filter_by_created_date(params[:created_after], params[:created_before])

    @relation
  end

  private

  def filter_by_name(name)
    @relation.where("name ILIKE ?", "%#{name}%")
  end

  def filter_by_email(email)
    @relation.where("email ILIKE ?", "%#{email}%")
  end

  def filter_by_role(role)
    @relation.where(role: role)
  end

  def filter_by_status(status)
    case status
    when 'active'
      @relation.where(active: true)
    when 'inactive'
      @relation.where(active: false)
    else
      @relation
    end
  end

  def filter_by_created_date(after_date, before_date)
    relation = @relation
    relation = relation.where("created_at >= ?", after_date) if after_date.present?
    relation = relation.where("created_at <= ?", before_date) if before_date.present?
    relation
  end
end

Usage:
```ruby

In controller

def index
@users = UserSearchQuery.new.search(search_params)
.page(params[:page])
.per(20)
end
```

Functional Patterns

4. Command Pattern

Encapsulate requests as objects, allowing you to parameterize and queue operations.

# app/commands/base_command.rb
class BaseCommand
  def self.call(*args)
    new(*args).call
  end

  def call
    raise NotImplementedError, "Subclasses must implement #call"
  end
end

# app/commands/send_notification_command.rb
class SendNotificationCommand < BaseCommand
  def initialize(user, message, type = :email)
    @user = user
    @message = message
    @type = type
  end

  def call
    case @type
    when :email
      send_email
    when :sms
      send_sms
    when :push
      send_push_notification
    else
      raise ArgumentError, "Unknown notification type: #{@type}"
    end
  end

  private

  def send_email
    UserMailer.notification(@user, @message).deliver_now
  end

  def send_sms
    SmsService.new.send(@user.phone, @message)
  end

  def send_push_notification
    PushNotificationService.new.send(@user.device_token, @message)
  end
end

Usage:
```ruby

Send different types of notifications

SendNotificationCommand.call(user, "Your order is ready!", :email)
SendNotificationCommand.call(user, "Urgent: Payment required", :sms)
```

5. Strategy Pattern

Define a family of algorithms and make them interchangeable.

# app/strategies/pricing_strategy.rb
module PricingStrategy
  class Base
    def initialize(order)
      @order = order
    end

    def calculate
      raise NotImplementedError
    end

    protected

    attr_reader :order
  end

  class RegularPricing < Base
    def calculate
      order.items.sum { |item| item.price * item.quantity }
    end
  end

  class BulkPricing < Base
    def calculate
      total = order.items.sum { |item| item.price * item.quantity }
      total > 1000 ? total * 0.9 : total
    end
  end

  class VipPricing < Base
    def calculate
      total = order.items.sum { |item| item.price * item.quantity }
      total * 0.8  # 20% discount for VIP customers
    end
  end
end

# app/models/order.rb
class Order < ApplicationRecord
  belongs_to :customer
  has_many :items

  def total_price
    pricing_strategy.calculate
  end

  private

  def pricing_strategy
    case customer.tier
    when 'vip'
      PricingStrategy::VipPricing.new(self)
    when 'bulk'
      PricingStrategy::BulkPricing.new(self)
    else
      PricingStrategy::RegularPricing.new(self)
    end
  end
end

6. Decorator Pattern

Add behavior to objects without altering their structure.

# app/decorators/user_decorator.rb
class UserDecorator < SimpleDelegator
  def display_name
    "#{first_name} #{last_name}"
  end

  def avatar_url(size = :medium)
    if avatar.present?
      avatar.variant(resize_to_limit: [avatar_size(size), nil])
    else
      default_avatar_url
    end
  end

  def status_badge
    css_class = active? ? "badge-success" : "badge-secondary"
    "<span class='badge #{css_class}'>#{status_text}</span>".html_safe
  end

  def membership_duration
    return "New member" unless created_at

    years = ((Time.current - created_at) / 1.year).floor
    return "Member for #{years} year#{'s' if years != 1}" if years > 0

    months = ((Time.current - created_at) / 1.month).floor
    "Member for #{months} month#{'s' if months != 1}"
  end

  private

  def avatar_size(size)
    case size
    when :small then 50
    when :medium then 100
    when :large then 200
    else 100
    end
  end

  def default_avatar_url
    "https://via.placeholder.com/#{avatar_size(:medium)}/cccccc/ffffff?text=#{initials}"
  end

  def initials
    [first_name, last_name].map(&:first).join.upcase
  end

  def status_text
    active? ? "Active" : "Inactive"
  end
end

Usage:
```ruby

In controller or view

decorateduser = UserDecorator.new(@user)
decorated
user.displayname
decorated
user.status_badge
```

Rails-Specific Patterns

7. Concerns Pattern

Share common functionality across multiple models or controllers.

# app/models/concerns/trackable.rb
module Trackable
  extend ActiveSupport::Concern

  included do
    scope :recent, -> { where('created_at > ?', 1.week.ago) }
    scope :old, -> { where('created_at < ?', 1.month.ago) }

    after_create :track_creation
    after_update :track_update
  end

  class_methods do
    def most_active(limit = 10)
      joins(:activities)
        .group('activities.trackable_id')
        .order('COUNT(activities.id) DESC')
        .limit(limit)
    end
  end

  def track_activity(action, metadata = {})
    activities.create!(
      action: action,
      metadata: metadata,
      occurred_at: Time.current
    )
  end

  def activity_count
    activities.count
  end

  def last_activity
    activities.order(:occurred_at).last
  end

  private

  def track_creation
    track_activity('created')
  end

  def track_update
    track_activity('updated', changes: previous_changes)
  end
end

# app/models/post.rb
class Post < ApplicationRecord
  include Trackable

  # Now has access to all Trackable functionality
end

# app/models/user.rb
class User < ApplicationRecord
  include Trackable

  # Also has access to all Trackable functionality
end

8. Builder Pattern

Construct complex objects step by step.

# app/builders/email_builder.rb
class EmailBuilder
  def initialize
    @email = {}
  end

  def to(recipient)
    @email[:to] = recipient
    self
  end

  def from(sender)
    @email[:from] = sender
    self
  end

  def subject(subject)
    @email[:subject] = subject
    self
  end

  def body(content)
    @email[:body] = content
    self
  end

  def template(template_name, variables = {})
    @email[:template] = template_name
    @email[:variables] = variables
    self
  end

  def priority(level)
    @email[:priority] = level
    self
  end

  def schedule_for(datetime)
    @email[:scheduled_for] = datetime
    self
  end

  def build
    validate_required_fields
    Email.new(@email)
  end

  def send!
    email = build
    EmailService.send(email)
  end

  private

  def validate_required_fields
    required_fields = [:to, :subject]
    missing_fields = required_fields.select { |field| @email[field].blank? }

    if missing_fields.any?
      raise ArgumentError, "Missing required fields: #{missing_fields.join(', ')}"
    end
  end
end

Usage:
```ruby

Fluent interface for building emails

EmailBuilder.new
.to('[email protected]')
.from('[email protected]')
.subject('Welcome to our app!')
.template('welcome_email', user: @user)
.priority(:high)
.send!

Or schedule for later

EmailBuilder.new
.to('[email protected]')
.subject('Weekly newsletter')
.template('newsletter')
.schedulefor(1.week.fromnow)
.send!
```

9. Repository Pattern

Encapsulate data access logic and provide a more object-oriented view of the persistence layer.

# app/repositories/user_repository.rb
class UserRepository
  def self.find_active_users
    User.where(active: true)
  end

  def self.find_by_role(role)
    User.where(role: role)
  end

  def self.find_recent_signups(days = 7)
    User.where('created_at > ?', days.days.ago)
  end

  def self.search_by_name_or_email(query)
    User.where(
      'name ILIKE :query OR email ILIKE :query',
      query: "%#{query}%"
    )
  end

  def self.find_with_posts
    User.includes(:posts).where.not(posts: { id: nil })
  end

  def self.top_contributors(limit = 10)
    User.joins(:posts)
        .group('users.id')
        .order('COUNT(posts.id) DESC')
        .limit(limit)
  end
end

10. Null Object Pattern

Provide default behavior for nil objects.

# app/models/null_user.rb
class NullUser
  def name
    "Guest User"
  end

  def email
    nil
  end

  def admin?
    false
  end

  def avatar
    nil
  end

  def posts
    Post.none
  end

  def display_name
    "Guest"
  end

  def persisted?
    false
  end

  def to_param
    nil
  end
end

# app/models/user.rb
class User < ApplicationRecord
  def self.find_or_null(id)
    find(id)
  rescue ActiveRecord::RecordNotFound
    NullUser.new
  end
end

Usage:
```ruby

In controller - no need to check for nil

def show
@user = User.findornull(params[:id])
# @user.name will always work, returning "Guest User" for null object
end

In view - no nil checks needed

<%= @user.name %> # Safe even if user not found
```

Best Practices for Ruby Patterns

1. Choose the Right Pattern

  • Use Service Objects for complex business logic
  • Use Form Objects for multi-model forms
  • Use Query Objects for complex database queries
  • Use Decorators for presentation logic

2. Keep It Simple

# Good - Simple and focused
class UserCreationService
  def initialize(params)
    @params = params
  end

  def call
    User.create(@params)
  end
end

# Avoid - Over-engineered
class UserCreationServiceFactoryBuilder
  # Too complex for simple user creation
end

3. Test Your Patterns

# test/services/user_registration_service_test.rb
class UserRegistrationServiceTest < ActiveSupport::TestCase
  test "successfully creates user and sends welcome email" do
    params = { name: "John", email: "[email protected]" }
    service = UserRegistrationService.new(params)

    assert service.call
    assert service.user.persisted?
    assert_emails 1
  end

  test "handles user creation failure" do
    params = { name: "", email: "invalid" }
    service = UserRegistrationService.new(params)

    assert_not service.call
    assert service.errors.any?
  end
end

Understanding and applying these Ruby patterns will help you write more maintainable, testable, and organized Rails applications. Start with the simpler patterns like Service Objects and gradually introduce more complex ones as your application grows.

Christopher Lim

Christopher Lim

Rails developer and Unity explorer. Family man, lifelong learner, and builder turning ideas into polished applications. Passionate about quality software development and continuous improvement.

Back to All Posts
Reading time: 8 min read

Enjoyed this rails development post?

Follow me for more insights on rails development, Rails development, and software engineering excellence.