“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?

  1. Testability: I can test the User model without worrying about side effects. I can test the UserRegistrationService by mocking the mailer.
  2. 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.
  3. 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.