This content originally appeared on DEV Community and was authored by Pratiksha Palkar
If you’re a Ruby on Rails developer, chances are you’re here because you care about writing cleaner, smarter code. Rails gives us a great starting point, but as apps grow, things can get messy fast — fat models, overstuffed controllers, and callbacks that feel like ticking time bombs.
That’s where design patterns step in. They’re not about adding complexity — they’re about giving your code structure, flexibility, and clarity.
The best part? You don’t need to learn all 20+ patterns. In real-world Rails projects, developers usually rely on just a handful.
In this post, we’ll explore 5 design patterns you’ll actually use in Rails — patterns that help refactor fat models, simplify controllers, and keep your app clean.
1. Strategy Pattern
The Problem - Imagine you’re building an e-commerce app in Rails. You need to calculate shipping costs differently depending on the type of delivery:
- Standard → cheap, slow
- Express → faster, more expensive
- International → much more expensive
The “quick and dirty” way is to put all logic in one method:
def calculate_shipping(order, type)
if type == "standard"
order.weight * 5
elsif type == "express"
order.weight * 10
elsif type == "international"
order.weight * 20
else
0
end
end
This works… but it’s messy. Every new shipping type means editing this method and adding more conditionals. Your code becomes hard to maintain and test.
The Solution - The Strategy Pattern says: instead of stuffing multiple behaviors into one method, split each behavior into its own class. Then, pick the right strategy at runtime.
Think of it like choosing how you travel: Walk, Drive, or Fly.Instead of cramming all travel rules into one function, you just pick the strategy: walking strategy, driving strategy, or flying strategy.
Rails Example
Step 1 – Create strategies
class StandardShipping
def calculate(order)
order.weight * 5
end
end
class ExpressShipping
def calculate(order)
order.weight * 10
end
end
class InternationalShipping
def calculate(order)
order.weight * 20
end
end
Step 2 – Create a calculator
class ShippingCalculator
def initialize(strategy)
@strategy = strategy
end
def calculate(order)
@strategy.calculate(order)
end
end
Step 3 – Use in a controller
class OrdersController < ApplicationController
def shipping_cost
order = Order.find(params[:id])
strategy = case params[:shipping_type]
when "standard" then StandardShipping.new
when "express" then ExpressShipping.new
when "international" then InternationalShipping.new
else StandardShipping.new
end
cost = ShippingCalculator.new(strategy).calculate(order)
render json: { cost: cost }
end
end
Why This Helps?
- No messy if/else blocks.
- Easy to add new shipping types (just create a new class).
- Each strategy is testable on its own.
- Your controllers/models stay clean and focused.
2. Decorator Pattern 🎨
Rails apps often suffer from fat models (too much business logic) and messy views (full of conditional display logic).
For example, let’s say you have a User model:
class User < ApplicationRecord
def full_name
"#{first_name} #{last_name}"
end
def formatted_date_of_joining
date_of_joining.strftime("%B %d, %Y")
end
def display_name
admin? ? "Admin: #{full_name}" : full_name
end
end
At first, this looks okay. But over time, models collect presentation logic (like formatting names and dates), which doesn’t really belong in the model. Views also become messy:
<!-- users/show.html.erb -->
<p>Welcome, <%= @user.admin? ? "Admin: #{@user.full_name}" : @user.full_name %></p>
<p>Joined on <%= @user.date_of_joining.strftime("%B %d, %Y") %></p>
Problems:
- Model is doing too much (mixing persistence logic with presentation logic).
- Views are full of conditionals and formatting code.
- Harder to test and maintain.
The Solution – Decorator Pattern
The Decorator Pattern says: Instead of stuffing presentation logic into the model or the view, create a decorator object that “wraps” the model and adds extra behavior.
How to Create a Decorator in Rails
Step 1 – Add Draper Gem
# Gemfile
gem 'draper'
Generate a decorator:
rails generate decorator User
Step 2 – Create the Decorator
class UserDecorator < Draper::Decorator
delegate_all # gives access to all User methods
def full_name
"#{object.first_name} #{object.last_name}"
end
def display_name
object.admin? ? "Admin: #{full_name}" : full_name
end
def formatted_date_of_joining
object.date_of_joining.strftime("%B %d, %Y")
end
end
Step 3 – Use in Controller
class UsersController < ApplicationController
def show
@user = User.find(params[:id]).decorate
end
end
Step 4 – Use in View
<p>Welcome, <%= @user.display_name %></p>
<p>Joined on <%= @user.formatted_date_of_joining %></p>
3. Observer Pattern
The Observer Pattern is a behavioral design pattern. When one object (called Subject) changes its state, all its dependents (Observers) are automatically notified and updated.
Think of YouTube:
- You (the user) subscribe to a channel (observer pattern in action!)
- The channel itself is the subject.
- Whenever the channel uploads a new video, all subscribers automatically get notified.
Notice:
- The channel doesn’t care who the subscribers are or how many there are.
- It just announces: “New video uploaded!”
- The subscribers decide what to do — watch, ignore, or share.
Example in Rails
Step 1 – Subject (Post)
class Post
attr_reader :title, :observers
def initialize(title)
@title = title
@observers = []
end
# Subscribe an observer
def add_observer(observer)
@observers << observer
end
# Notify all observers when something happens
def publish
puts "Publishing post: #{title}"
@observers.each { |observer| observer.update(self) }
end
end
Step 2 – Observers
class EmailNotifier
def update(post)
puts "EmailNotifier: A new post titled '#{post.title}' was published!"
end
end
class Logger
def update(post)
puts "Logger: Post '#{post.title}' has been published."
end
end
Step 3 – Use the Pattern
# Create post
post = Post.new("Observer Pattern in Rails")
# Add observers
post.add_observer(EmailNotifier.new)
post.add_observer(Logger.new)
# Publish post
post.publish
Output
Publishing post: Observer Pattern in Rails
EmailNotifier: A new post titled 'Observer Pattern in Rails' was published!
Logger: Post 'Observer Pattern in Rails' has been published.
Why This Works
- Post doesn’t need to know what observers do — it just notifies them.
- Observers (EmailNotifier, Logger) handle their own logic.
- Adding new observers is easy: just post.add_observer(NewObserver.new).
4. Singleton Pattern 🔒
Sometimes in an application, you only ever want one instance of a class.
Examples:
- You don’t want 10 different loggers writing files differently, you want one logger used everywhere.
- You don’t want multiple configuration loaders — just one config manager for the whole app.
- You don’t want duplicate cache objects — one cache should be shared.
The Singleton Pattern ensures:
- Only one instance of a class can exist.
- You can access that instance globally across your app.
In Ruby, it’s super easy because Ruby gives us the Singleton module.
Example in Ruby
require 'singleton'
class AppConfig
include Singleton
attr_accessor :settings
def initialize
@settings = { app_name: "MyApp", version: "1.0" }
end
end
# Usage
config1 = AppConfig.instance
config2 = AppConfig.instance
config1.settings[:app_name] = "MySuperApp"
puts config2.settings[:app_name]
# => "MySuperApp" (same instance!)
Notice how config1 and config2 are the same object.
If you try AppConfig.new, Ruby will throw an error — you must use .instance.
How We Use Singleton in Rails
Example 1: Rails Logger
Rails already uses Singleton!
Rails.logger.info "User signed up"
No matter where you call Rails.logger, it’s the same logger instance. Imagine the chaos if every controller made its own logger file.
Example 2: Cache Store
Rails.cache.write("foo", "bar")
Rails.cache.read("foo") # => "bar"
Rails.cache is a Singleton. Same cache everywhere in the app.
Example 3: Global Configuration
You can create your own Singleton for app-wide settings:
# app/services/global_config.rb
require 'singleton'
class GlobalConfig
include Singleton
def db_connection_string
ENV["DB_CONNECTION"]
end
end
# usage anywhere
GlobalConfig.instance.db_connection_string
So, whenever you find yourself needing a single shared resource in Rails (logger, cache, config), Singleton is your friend.
5. Facade Pattern
Imagine you’re building a job portal (similar to Naukri/LinkedIn).
When a recruiter posts a new job, multiple things need to happen:
- Save the job in the database.
- Notify subscribed candidates.
- Send a confirmation email to the recruiter.
- Update activities for reporting.
- Push the job to third-party platforms.
If you put all of this inside your JobsController, it might look like this:
class JobsController < ApplicationController
def create
@job = Job.new(job_params)
if @job.save
CandidateNotifier.notify(@job)
RecruiterMailer.job_posted(@job).deliver_later
Activity.track("job_posted", @job.id)
ThirdPartyPoster.push(@job)
redirect_to @job, notice: "Job posted successfully!"
else
render :new
end
end
end
What’s wrong here?
- Controller is doing too much.
- Hard to test (you’d need to stub email, activities, third-party, etc. every time).
- If requirements change (e.g., also post to Slack), you’d update this controller, making it even messier.
The Solution: Facade Pattern
Instead of letting the controller know all the details, we create a Facade class that hides the complexity.
Step 1: Create a Facade
# app/facades/job_posting_facade.rb
class JobPostingFacade
def self.post_job(job_params)
job = Job.new(job_params)
return nil unless job.save
CandidateNotifier.notify(job)
RecruiterMailer.job_posted(job).deliver_later
Activity.track("job_posted", job.id)
ThirdPartyPoster.push(job)
job
end
end
Step 2: Use Facade in the Controller
class JobsController < ApplicationController
def create
@job = JobPostingFacade.post_job(job_params)
if @job
redirect_to @job, notice: "Job posted successfully!"
else
render :new
end
end
end
Why This Is Better
- Controller is clean: it only knows "post a job".
- Signup workflow is centralized: all steps live in JobPostingFacade.
- If business logic changes (e.g., no more third-party posting), you only edit the facade.
- Easier to test: you can test JobPostingFacade separately.
📌 Summary / Key Takeaways
- Strategy Pattern → Choose different behaviors without messy if-else.
- Decorator Pattern → Add extra features to objects without changing the original code.
- Observer Pattern → Notify many parts of the app automatically when something happens.
- Singleton Pattern → Ensure only one instance of a resource exists.
- Facade Pattern → Simplify complex workflows behind one clean interface.
Final Thoughts ✨
Design patterns are not just theory — they are practical tools to make Rails apps more maintainable, testable, and clean.
Next time your Rails app feels messy, ask yourself:
- Is there a strategy hiding in all these if/else blocks?
- Are models doing too much presentation work?
- Are multiple parts of my app reacting to the same change?
- Do I need a single shared resource?
- Am I repeating the same workflow across controllers?
Refactor with these 5 patterns, and your code will thank you.
This content originally appeared on DEV Community and was authored by Pratiksha Palkar

Pratiksha Palkar | Sciencx (2025-09-19T03:55:32+00:00) From Fat Models to Clean Code: 5 Practical Design Patterns in Ruby on Rails. Retrieved from https://www.scien.cx/2025/09/19/from-fat-models-to-clean-code-5-practical-design-patterns-in-ruby-on-rails-2/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.