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.