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.
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.