From Fat Models to Clean Code: 5 Practical Design Patterns in Ruby on Rails

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 t…


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


Print Share Comment Cite Upload Translate Updates
APA

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/

MLA
" » From Fat Models to Clean Code: 5 Practical Design Patterns in Ruby on Rails." Pratiksha Palkar | Sciencx - Friday September 19, 2025, https://www.scien.cx/2025/09/19/from-fat-models-to-clean-code-5-practical-design-patterns-in-ruby-on-rails-2/
HARVARD
Pratiksha Palkar | Sciencx Friday September 19, 2025 » From Fat Models to Clean Code: 5 Practical Design Patterns in Ruby on Rails., viewed ,<https://www.scien.cx/2025/09/19/from-fat-models-to-clean-code-5-practical-design-patterns-in-ruby-on-rails-2/>
VANCOUVER
Pratiksha Palkar | Sciencx - » From Fat Models to Clean Code: 5 Practical Design Patterns in Ruby on Rails. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/09/19/from-fat-models-to-clean-code-5-practical-design-patterns-in-ruby-on-rails-2/
CHICAGO
" » From Fat Models to Clean Code: 5 Practical Design Patterns in Ruby on Rails." Pratiksha Palkar | Sciencx - Accessed . https://www.scien.cx/2025/09/19/from-fat-models-to-clean-code-5-practical-design-patterns-in-ruby-on-rails-2/
IEEE
" » From Fat Models to Clean Code: 5 Practical Design Patterns in Ruby on Rails." Pratiksha Palkar | Sciencx [Online]. Available: https://www.scien.cx/2025/09/19/from-fat-models-to-clean-code-5-practical-design-patterns-in-ruby-on-rails-2/. [Accessed: ]
rf:citation
» From Fat Models to Clean Code: 5 Practical Design Patterns in Ruby on Rails | Pratiksha Palkar | Sciencx | 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.

You must be logged in to translate posts. Please log in or register.