Ruby on Rails Design Patterns

πŸš€ Ruby on Rails Design Patterns You Must Know for Clean & Scalable Code

When working with Ruby on Rails (RoR), developers often face challenges like keeping code clean, avoiding repetition, and managing complexity as applications grow. That’s where Design Patterns step in πŸ‘¨β€πŸ’».

Design Patterns are proven solutions to common problems in software development. They don’t reinvent the wheel but guide us toward structured, reusable, and maintainable code. In this blog, we’ll dive deep into Rails-specific design patterns, their use cases, and real examples.

ruby


1️⃣ Service Object Pattern πŸ’Ό

πŸ“Œ Problem:

Your controllers or models are doing too muchβ€”business logic is scattered everywhere.

πŸ“Œ Solution:

Extract business logic into Service Objects. This makes controllers thinner and models focused only on persistence.

βœ… Example:

# app/services/user_signup_service.rb
class UserSignupService
  def initialize(user_params)
    @user_params = user_params
  end

  def call
    user = User.new(@user_params)
    if user.save
      WelcomeMailer.send_email(user).deliver_later
      user
    else
      nil
    end
  end
end
# app/controllers/users_controller.rb
def create
  @user = UserSignupService.new(user_params).call
  if @user
    redirect_to dashboard_path, notice: "πŸŽ‰ Signup successful!"
  else
    render :new
  end
end

πŸ‘‰ Keeps the controller lean and business logic reusable.


2️⃣ Decorator Pattern 🎭

πŸ“Œ Problem:

You want to add presentation-related logic (formatting, display) without cluttering models or views.

πŸ“Œ Solution:

Use Decorators to wrap models and add UI-specific behavior.

βœ… Example (with draper gem):

# app/decorators/user_decorator.rb
class UserDecorator < Draper::Decorator
  delegate_all

  def display_name
    "#{object.first_name} #{object.last_name}".titleize
  end

  def joined_date
    object.created_at.strftime("%B %d, %Y")
  end
end
<!-- In View -->
<p><%= @user.decorate.display_name %></p>
<p>Joined: <%= @user.decorate.joined_date %></p>

πŸ‘‰ Keeps models clean while enhancing UI representation.


3️⃣ Query Object Pattern πŸ”

πŸ“Œ Problem:

Complex queries with ActiveRecord clutter controllers or models.

πŸ“Œ Solution:

Encapsulate queries into Query Objects for readability and reusability.

βœ… Example:

# app/queries/active_users_query.rb
class ActiveUsersQuery
  def self.call
    User.where(active: true).where("last_login_at >= ?", 30.days.ago)
  end
end
# In Controller
@users = ActiveUsersQuery.call

πŸ‘‰ Makes database logic reusable and easier to maintain.


4️⃣ Form Object Pattern πŸ“

πŸ“Œ Problem:

You have forms that update multiple models (e.g., User + Profile). Handling validation across them gets messy.

πŸ“Œ Solution:

Use Form Objects to encapsulate form-specific validations and persistence.

βœ… Example:

# app/forms/signup_form.rb
class SignupForm
  include ActiveModel::Model

  attr_accessor :user_name, :email, :profile_bio

  validates :user_name, :email, presence: true

  def save
    return false unless valid?
    user = User.create!(name: user_name, email: email)
    Profile.create!(user: user, bio: profile_bio)
  end
end
# In Controller
form = SignupForm.new(params[:signup])
if form.save
  redirect_to dashboard_path, notice: "Account created βœ…"
else
  render :new
end

πŸ‘‰ Simplifies multi-model forms and keeps controllers DRY.


5️⃣ Policy Object Pattern πŸ”‘

πŸ“Œ Problem:

Authorization logic scattered across controllers and views.

πŸ“Œ Solution:

Extract permission rules into Policy Objects (e.g., using pundit gem).

βœ… Example:

# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def update?
    user.admin? || record.user == user
  end
end
# In Controller
def update
  authorize @post
  @post.update(post_params)
end

πŸ‘‰ Centralizes authorization logic for better security & clarity.


6️⃣ Presenter Pattern 🎀

πŸ“Œ Problem:

Views need multiple helper methods and formatting. Helpers get bloated.

πŸ“Œ Solution:

Use Presenter Objects to represent data in a more structured way.

βœ… Example:

# app/presenters/order_presenter.rb
class OrderPresenter
  def initialize(order)
    @order = order
  end

  def formatted_total
    "$#{'%.2f' % @order.total}"
  end

  def delivery_status
    @order.delivered? ? "🚚 Delivered" : "⏳ In Progress"
  end
end
<!-- In View -->
<p><%= OrderPresenter.new(@order).formatted_total %></p>
<p><%= OrderPresenter.new(@order).delivery_status %></p>

πŸ‘‰ A clean way to keep view logic separate from controllers.


7️⃣ Observer Pattern πŸ‘€

πŸ“Œ Problem:

You want to trigger background tasks (like sending emails or analytics) without bloating model callbacks.

πŸ“Œ Solution:

Use Observers (or ActiveSupport::Notifications) to listen to events and respond.

βœ… Example:

# app/observers/user_observer.rb
class UserObserver < ActiveRecord::Observer
  def after_create(user)
    Analytics.track("User created", user_id: user.id)
  end
end

πŸ‘‰ Keeps models focused while still responding to lifecycle events.


🎯 Bonus Tips for Perfect Use of Design Patterns

βœ… Keep Controllers Skinny β†’ Use Service Objects and Query Objects. βœ… Keep Models Focused β†’ Avoid business logic inside models. βœ… Use the Right Pattern β†’ Don’t over-engineer. Choose a pattern only when needed. βœ… Consistency is Key β†’ Stick to one approach across your app. βœ… Refactor Early β†’ Don’t wait until your app gets messy.


✨ Final Thoughts

Rails provides conventions that already reduce boilerplate, but when your app grows, these Design Patterns help keep things organized, scalable, and clean.

Adopting Service Objects, Query Objects, Decorators, and Policies can level up your Rails codebase πŸ’‘.

πŸš€ So next time your controller feels bloated or your model looks messy, remember: there’s a design pattern for that!

© Lakhveer Singh Rajput - Blogs. All Rights Reserved.