“Fat models, skinny controllers.” That was the mantra for years. But if you’ve ever worked on a Rails app that’s more than two years old, you know where that leads: a User.rb file that’s 2,000 lines long and handles everything from authentication to generating PDF invoices.
The Problem with Fat Models
When a model knows too much, it becomes impossible to test in isolation. You want to test a simple validation, but you end up triggering five callbacks that send emails and ping Slack hooks.
Enter Service Objects (and Plain Old Ruby Objects)
I prefer keeping my models as thin as possible—ideally just associations and simple validations. For everything else, I use Service Objects.
Instead of:
# In the model
def register_and_notify
save!
UserMailer.welcome(self).deliver_later
SlackService.notify("New user: #{email}")
end
I do this:
# In a service object
class UserRegistrationService
def initialize(params)
@params = params
end
def call
user = User.new(@params)
if user.save
notify_user(user)
notify_team(user)
end
user
end
private
# ... helper methods
end
Why bother?
- Testability: I can test the
Usermodel without worrying about side effects. I can test theUserRegistrationServiceby mocking the mailer. - Readability: When I open a model, I see what the data is. When I look at the
services/folder, I see what the app does. - Reusability: Need to register a user via the API and the Web? Use the same service.
Keep your models thin, your services focused, and your sanity intact.