Rails Development

Rails Refactoring Examples: Transform Messy Code into Clean, Maintainable Solutions

Refactoring is the process of improving code structure without changing its functionality. Here are practical refactoring examples that will help you recognize code smells and transform them into clean, maintainable Rails code.

Controller Refactoring

1. Fat Controller → Skinny Controller with Service Objects

Before (Fat Controller):
```ruby
class OrdersController < ApplicationController
def create
@order = Order.new(orderparams)
@order.user = current
user
@order.status = 'pending'
@order.ordernumber = generateorder_number

if @order.save
# Calculate total with tax and shipping
subtotal = @order.items.sum { |item| item.price * item.quantity }
tax = subtotal * 0.08
shipping = subtotal > 50 ? 0 : 10
@order.update(
subtotal: subtotal,
tax: tax,
shipping: shipping,
total: subtotal + tax + shipping
)

# Update inventory
@order.items.each do |item|
product = item.product
product.update(stock: product.stock - item.quantity)

# Send low stock alert
if product.stock < 5
AdminMailer.lowstockalert(product).deliver_now
end
end

# Send confirmation email
OrderMailer.confirmation(@order).deliver_now

# Create loyalty points
points = (@order.total * 0.1).round
currentuser.loyaltypoints += points
current_user.save

redirect_to @order, notice: 'Order created successfully!'
else
render :new
end
end

private

def generateordernumber
"ORD-#{Time.current.strftime('%Y%m%d')}-#{SecureRandom.hex(4).upcase}"
end
end
```

After (Skinny Controller with Service Object):
```ruby

app/controllers/orders_controller.rb

class OrdersController < ApplicationController
def create
service = OrderCreationService.new(currentuser, orderparams)

if service.call
redirect_to service.order, notice: 'Order created successfully!'
else
@order = service.order
render :new
end
end
end

app/services/ordercreationservice.rb

class OrderCreationService
attr_reader :order, :errors

def initialize(user, orderparams)
@user = user
@order
params = order_params
@errors = []
end

def call
ActiveRecord::Base.transaction do
createorder
calculate
totals
updateinventory
award
loyaltypoints
send
confirmation_email
end

order.persisted? && errors.empty?
rescue => e
@errors << e.message
false
end

private

attrreader :user, :orderparams

def createorder
@order = Order.create!(
order
params.merge(
user: user,
status: 'pending',
order_number: OrderNumberGenerator.generate
)
)
end

def calculate_totals
calculator = OrderTotalCalculator.new(@order)
calculator.calculate!
end

def update_inventory
InventoryUpdateService.new(@order).call
end

def awardloyaltypoints
LoyaltyPointsService.new(user, @order.total).award_points
end

def sendconfirmationemail
OrderMailer.confirmation(@order).deliver_later
end
end
```

2. Complex Conditional Logic → Strategy Pattern

Before:
```ruby
class PaymentController < ApplicationController
def process
case params[:paymentmethod]
when 'credit
card'
if params[:cardnumber].length == 16
charge = Stripe::Charge.create(
amount: (params[:amount].to
f * 100).to_i,
currency: 'usd',
source: params[:token]
)

if charge.paid
Payment.create!(
amount: params[:amount],
method: 'creditcard',
external
id: charge.id,
status: 'completed'
)
redirectto successpath
else
flash[:error] = 'Payment failed'
redirectto paymentpath
end
else
flash[:error] = 'Invalid card number'
redirectto paymentpath
end

when 'paypal'
# PayPal processing logic...
response = PayPal::SDK::REST::Payment.new({
intent: 'sale',
payer: { payment_method: 'paypal' },
# ... complex PayPal setup
})

if response.create
# Handle success
else
# Handle failure
end

when 'bank_transfer'
# Bank transfer logic...
end
end
end
```

After:
```ruby

app/controllers/payment_controller.rb

class PaymentController < ApplicationController
def process
processor = PaymentProcessorFactory.create(
params[:paymentmethod],
payment
params
)

result = processor.process

if result.success?
redirectto successpath, notice: 'Payment successful!'
else
flash[:error] = result.errormessage
redirect
to payment_path
end
end
end

app/services/paymentprocessorfactory.rb

class PaymentProcessorFactory
def self.create(method, params)
case method
when 'creditcard'
CreditCardProcessor.new(params)
when 'paypal'
PaypalProcessor.new(params)
when 'bank
transfer'
BankTransferProcessor.new(params)
else
raise ArgumentError, "Unknown payment method: #{method}"
end
end
end

app/services/creditcardprocessor.rb

class CreditCardProcessor
def initialize(params)
@params = params
end

def process
return failureresult('Invalid card number') unless validcard?

charge = createstripecharge

if charge.paid
createpaymentrecord(charge)
successresult
else
failure
result('Payment failed')
end
rescue Stripe::CardError => e
failure_result(e.message)
end

private

def validcard?
@params[:card
number]&.length == 16
end

def createstripecharge
Stripe::Charge.create(
amount: (@params[:amount].tof * 100).toi,
currency: 'usd',
source: @params[:token]
)
end

def createpaymentrecord(charge)
Payment.create!(
amount: @params[:amount],
method: 'creditcard',
external
id: charge.id,
status: 'completed'
)
end

def success_result
PaymentResult.new(success: true)
end

def failureresult(message)
PaymentResult.new(success: false, error
message: message)
end
end

app/models/payment_result.rb

class PaymentResult
attrreader :errormessage

def initialize(success:, errormessage: nil)
@success = success
@error
message = error_message
end

def success?
@success
end
end
```

Model Refactoring

3. Fat Model → Skinny Model with Concerns

Before:
```ruby
class User < ApplicationRecord
hasmany :posts
has
many :comments

# Authentication methods
def authenticate(password)
BCrypt::Password.new(password_digest) == password
end

def generatepasswordresettoken
self.password
resettoken = SecureRandom.urlsafebase64
self.passwordresetsent_at = Time.current
save!
end

def passwordresetexpired?
passwordresetsent_at < 2.hours.ago
end

# Avatar methods
def avatarurl(size = :medium)
if avatar.present?
avatar.variant(resize
tolimit: [avatarsize(size), nil])
else
defaultavatarurl(size)
end
end

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

def defaultavatarurl(size)
"https://via.placeholder.com/#{avatar_size(size)}"
end

# Activity tracking
def tracklogin
update(last
loginat: Time.current, logincount: login_count + 1)
end

def activeuser?
last
login_at > 30.days.ago
end

def totalpoststhismonth
posts.where('created
at > ?', 1.month.ago).count
end

# Notification methods
def notify(message, type = :info)
notifications.create!(
message: message,
notification_type: type,
read: false
)
end

def unreadnotificationscount
notifications.where(read: false).count
end

def markallnotificationsread
notifications.where(read: false).update
all(read: true)
end
end
```

After:
```ruby

app/models/user.rb

class User < ApplicationRecord
include Authenticatable
include Avatareable
include Trackable
include Notifiable

hasmany :posts
has
many :comments
end

app/models/concerns/authenticatable.rb

module Authenticatable
extend ActiveSupport::Concern

def authenticate(password)
BCrypt::Password.new(password_digest) == password
end

def generatepasswordresettoken
self.password
resettoken = SecureRandom.urlsafebase64
self.passwordresetsent_at = Time.current
save!
end

def passwordresetexpired?
passwordresetsent_at < 2.hours.ago
end
end

app/models/concerns/avatareable.rb

module Avatareable
extend ActiveSupport::Concern

def avatarurl(size = :medium)
if avatar.present?
avatar.variant(resize
tolimit: [avatarsize(size), nil])
else
defaultavatarurl(size)
end
end

private

def avatar_size(size)
{ small: 50, medium: 100, large: 200 }[size] || 100
end

def defaultavatarurl(size)
"https://via.placeholder.com/#{avatar_size(size)}"
end
end

app/models/concerns/trackable.rb

module Trackable
extend ActiveSupport::Concern

def tracklogin
update(last
loginat: Time.current, logincount: login_count + 1)
end

def activeuser?
last
login_at > 30.days.ago
end

def totalpoststhismonth
posts.where('created
at > ?', 1.month.ago).count
end
end

app/models/concerns/notifiable.rb

module Notifiable
extend ActiveSupport::Concern

included do
has_many :notifications, dependent: :destroy
end

def notify(message, type = :info)
notifications.create!(
message: message,
notification_type: type,
read: false
)
end

def unreadnotificationscount
notifications.where(read: false).count
end

def markallnotificationsread
notifications.where(read: false).update
all(read: true)
end
end
```

4. N+1 Query Problem → Eager Loading and Query Objects

Before:
```ruby
class PostsController < ApplicationController
def index
@posts = Post.published.limit(20)
# This creates N+1 queries:
# 1 query for posts
# N queries for each post's author
# N queries for each post's comments count
end
end

In the view:

<% @posts.each do |post| %>

<%= post.title %>

by <%= post.author.name %> # N+1 query here

<%= post.comments.count %> comments # N+1 query here

<% end %>


**After:**
```ruby
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = PostQuery.new.published_with_details.limit(20)
  end
end

# app/queries/post_query.rb
class PostQuery
  def initialize(relation = Post.all)
    @relation = relation
  end

  def published_with_details
    @relation.published
             .includes(:author, :tags)
             .left_joins(:comments)
             .select('posts.*, COUNT(comments.id) as comments_count')
             .group('posts.id')
             .order(created_at: :desc)
  end

  def by_author(author)
    @relation.where(author: author)
  end

  def recent(days = 7)
    @relation.where('created_at > ?', days.days.ago)
  end
end

# Or using a scope in the model:
# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments
  has_and_belongs_to_many :tags

  scope :published, -> { where(published: true) }
  scope :with_details, -> { 
    includes(:author, :tags)
      .left_joins(:comments)
      .select('posts.*, COUNT(comments.id) as comments_count')
      .group('posts.id')
  }
end

5. Callback Hell → Service Objects and Observers

Before:
```ruby
class Order < ApplicationRecord
belongsto :user
has
many :items

beforesave :calculatetotal
aftercreate :sendconfirmationemail
after
create :updateinventory
after
create :createshipment
after
create :updateuserstats
aftercreate :notifyadminiflargeorder
after
update :handlestatuschange

private

def calculatetotal
self.total = items.sum { |item| item.price * item.quantity }
self.tax = total * 0.08
self.total
with_tax = total + tax
end

def sendconfirmationemail
OrderMailer.confirmation(self).deliver_now
end

def update_inventory
items.each do |item|
item.product.decrement!(:stock, item.quantity)
end
end

def create_shipment
Shipment.create!(order: self, status: 'preparing')
end

def updateuserstats
user.increment!(:totalorders)
user.update!(last
order_at: Time.current)
end

def notifyadminiflargeorder
AdminMailer.largeorderalert(self).deliver_now if total > 1000
end

def handlestatuschange
if statuschanged? && status == 'shipped'
OrderMailer.shipped
notification(self).delivernow
TrackingService.create
tracking_number(self)
end
end
end
```

After:
```ruby

app/models/order.rb

class Order < ApplicationRecord
belongsto :user
has
many :items

beforesave :calculatetotal

# Use a service object instead of callbacks
def process!
OrderProcessingService.new(self).process!
end

private

def calculate_total
calculator = OrderTotalCalculator.new(self)
calculator.calculate

self.total = calculator.total
self.tax = calculator.tax
self.totalwithtax = calculator.totalwithtax
end
end

app/services/orderprocessingservice.rb

class OrderProcessingService
def initialize(order)
@order = order
end

def process!
ActiveRecord::Base.transaction do
sendconfirmationemail
updateinventory
create
shipment
updateuserstats
notifyadminif_needed
end
end

private

attr_reader :order

def sendconfirmationemail
OrderMailer.confirmation(order).deliver_later
end

def update_inventory
InventoryUpdateService.new(order).call
end

def create_shipment
ShipmentCreationService.new(order).call
end

def updateuserstats
UserStatsUpdateService.new(order.user, order).call
end

def notifyadminifneeded
AdminNotificationService.new(order).notify
iflargeorder
end
end

app/observers/order_observer.rb (if using observers)

class OrderObserver < ActiveRecord::Observer
def afterupdate(order)
return unless order.status
changed?

case order.status
when 'shipped'
handleshipment(order)
when 'cancelled'
handle
cancellation(order)
end
end

private

def handleshipment(order)
OrderMailer.shipped
notification(order).deliverlater
TrackingService.create
tracking_number(order)
end

def handlecancellation(order)
InventoryRestoreService.new(order).call
RefundService.new(order).process
refund
end
end
```

View Refactoring

6. Complex Views → Partials and Helpers

Before:
```erb
<!-- app/views/users/show.html.erb -->


<% if @user.avatar.present? %>
<%= imagetag @user.avatar, class: "avatar", size: "100x100" %>
<% else %>

<%= @user.first
name.first + @user.last_name.first %>

<% end %>

<%= @user.firstname %> <%= @user.lastname %>

<% if @user.active? %>
Active
<% else %>
Inactive
<% end %>

Member since:
<% if @user.createdat > 1.year.ago %>
<%= @user.created
at.strftime("%B %Y") %>
<% else %>
<%= @user.created_at.strftime("%B %d, %Y") %>
<% end %>

<%= @user.posts.count %>
Posts


<%= @user.comments.count %>
Comments



<% totallikes = @user.posts.joins(:likes).count %>
<%= total
likes %>

Likes Received

Recent Posts
<% @user.posts.recent.limit(5).each do |post| %>

<%= linkto post.title, post %>
<%= truncate(post.content, length: 150) %>

Posted <%= time
agoinwords(post.created_at) %> ago
• <%= post.comments.count %> comments
• <%= post.likes.count %> likes


<% end %>


```

After:
```erb
<!-- app/views/users/show.html.erb -->

<%= render 'userheader', user: @user %>
<%= render 'user
stats', user: @user %>
<%= render 'recent_posts', posts: @user.posts.recent.limit(5) %>

<!-- app/views/users/userheader.html.erb -->

<%= useravatar(user, size: 100) %>
<%= user.display
name %>
<%= userstatusbadge(user) %>
Member since: <%= membership_duration(user) %>

<!-- app/views/users/userstats.html.erb -->

<%= render 'statitem', number: user.posts.count, label: 'Posts' %>
<%= render 'stat
item', number: user.comments.count, label: 'Comments' %>
<%= render 'statitem', number: user.totallikes_received, label: 'Likes Received' %>

<!-- app/views/users/statitem.html.erb -->

<%= number %>
<%= label %>

<!-- app/views/users/recentposts.html.erb -->

Recent Posts
<%= render posts if posts.any? %>

<!-- app/views/posts/post.html.erb -->

<%= link
to post.title, post %>
<%= truncatetext(post.content, 150) %>
<%= render 'post
meta', post: post %>

<!-- app/views/posts/postmeta.html.erb -->

Posted <%= friendlytimestamp(post.createdat) %>
• <%= pluralize(post.comments.count, 'comment') %>
• <%= pluralize(post.likes.count, 'like') %>

```

With corresponding helpers:
```ruby

app/helpers/users_helper.rb

module UsersHelper
def useravatar(user, size: 50)
if user.avatar.present?
image
tag user.avatar, class: "avatar rounded-circle", size: "#{size}x#{size}"
else
content_tag :div, user.initials,
class: "avatar-placeholder rounded-circle",
style: "width: #{size}px; height: #{size}px;"
end
end

def userstatusbadge(user)
cssclass = user.active? ? "badge-success" : "badge-secondary"
status
text = user.active? ? "Active" : "Inactive"
contenttag :span, statustext, class: "badge #{css_class}"
end

def membershipduration(user)
if user.created
at > 1.year.ago
user.createdat.strftime("%B %Y")
else
user.created
at.strftime("%B %d, %Y")
end
end
end

Add to User model:

class User < ApplicationRecord
def displayname
"#{first
name} #{last_name}"
end

def initials
"#{firstname.first}#{lastname.first}".upcase
end

def totallikesreceived
posts.joins(:likes).count
end
end
```

Refactoring Best Practices

1. Start Small

  • Refactor one method at a time
  • Make small, incremental changes
  • Test after each change

2. Identify Code Smells

  • Long methods (> 10 lines)
  • Large classes (> 100 lines)
  • Duplicate code
  • Long parameter lists
  • Complex conditionals

3. Use Rails Conventions

  • Follow the Rails Way
  • Use existing Rails patterns
  • Leverage Rails' built-in methods

4. Always Test

# Before refactoring, write tests to ensure behavior doesn't change
class OrderTest < ActiveSupport::TestCase
  test "order total calculation includes tax" do
    order = orders(:sample_order)
    order.calculate_total

    expected_total = order.items.sum { |item| item.price * item.quantity }
    expected_tax = expected_total * 0.08

    assert_equal expected_total, order.total
    assert_equal expected_tax, order.tax
    assert_equal expected_total + expected_tax, order.total_with_tax
  end
end

5. Refactor in Steps

  1. Extract Method: Break large methods into smaller ones
  2. Extract Class: Move related methods to a new class
  3. Replace Conditional with Polymorphism: Use inheritance or strategy pattern
  4. Move Method: Move methods to more appropriate classes
  5. Rename: Use clear, descriptive names

Remember: refactoring is about improving code structure while maintaining functionality. Always have good test coverage before refactoring, and make small changes incrementally.

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: 10 min read

Enjoyed this rails development post?

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