Rails AntiPatterns in Models – and How to Fix Them

Rails encourages clean separation of concerns, but it’s easy to let logic leak into the wrong layer. The model is meant to encapsulate business rules and domain behavior, yet many applications end up with fat controllers, bloated views, or overly complex models. Below are common anti-patterns in Rails models and their solutions, with code.


1. View Logic Belongs in the Model

A common mistake is putting conditional business logic directly in views.

Bad:

<% if @order.total > 100 %>
  <p>Premium Order</p>
<% end %>

Better: Move the rule into the model.

class Order < ApplicationRecord
  def premium?
    total > 100
  end
end
<% if @order.premium? %>
  <p>Premium Order</p>
<% end %>

2. Callbacks That Do Too Much

Callbacks are useful, but overusing them hides important behavior.

Bad:

class User < ApplicationRecord
  after_create :send_welcome_email

  def send_welcome_email
    Mailer.welcome(self).deliver_now
  end
end

Better: Extract into a service object.

class UserSignup
  def self.call(user)
    Mailer.welcome(user).deliver_now
  end
end
user = User.create!(params)
UserSignup.call(user)

3. SQL in Controllers

Query logic clutters controllers and scatters business rules.

Bad:

@active_users = User.where("last_login > ?", 30.days.ago)

Better: Use a scope in the model.

class User < ApplicationRecord
  scope :recently_active, -> { where("last_login > ?", 30.days.ago) }
end
@active_users = User.recently_active

4. Serialized Attributes Instead of Tables

Dumping hashes into [serialize](https://api.rubyonrails.org/classes/ActiveModel/Serialization.html) columns makes querying hard.

Bad:

class User < ApplicationRecord
  serialize :preferences, Hash
end

Better: Normalize with an association.

class Preference < ApplicationRecord
  belongs_to :user
end

class User < ApplicationRecord
  has_many :preferences
end

5. Abusing Single Table Inheritance

STI forces unrelated subclasses into one table, often with many NULL columns.

Bad:

class Payment < ApplicationRecord; end
class CreditCardPayment < Payment; end
class BankTransferPayment < Payment; end

Better: Use polymorphism or composition.

class Payment < ApplicationRecord
  belongs_to :payable, polymorphic: true
end

class CreditCard < ApplicationRecord
  has_many :payments, as: :payable
end

This post was inspired by the book Rails AntiPatterns: Best Practice Ruby on Rails Refactoring by Chad Pytel and Tammer Saleh. If you’d like to read the full book, you can find it on Amazon: Rails AntiPatterns

If you buy the book through that Amazon link, I may earn a small commission at no extra cost to you — thanks for supporting this content.


Key Takeaway

  • Move business rules into models (not views or controllers).
  • Use service objects when callbacks hide too much.
  • Keep SQL in scopes, not scattered queries.
  • Normalize data structures, avoid serialized blobs.
  • Avoid abusing STI; prefer polymorphic or compositional design.

By refactoring these anti-patterns, your models stay expressive, your controllers thin, and your views clean.