Fat Model / Skinny Controller

Fat Model / Skinny Controller is a design philosophy in Ruby on Rails that encourages keeping controllers lightweight by placing business logic in models or, when appropriate, service objects.

Table of Contents

What Is Fat Model / Skinny Controller?

Rails follows the MVC (Model-View-Controller) pattern. Over time, developers found that placing too much logic directly in controllers made applications harder to test, maintain, and reuse. The Fat Model / Skinny Controller philosophy addresses this by:

  • Keeping controllers focused on request handling, params, and rendering responses
  • Moving validations, calculations, and business rules into models
  • Extracting complex or cross-model logic into service objects or concerns when it doesn't belong in a single model

The result is controllers that are easy to read at a glance, and business logic that's testable independently of HTTP requests.

Why Is It Useful?

Without this separation, controllers accumulate logic that's difficult to reuse or test, leading to:

  • Duplicated logic across multiple controller actions
  • Difficult-to-write unit tests (since testing requires simulating full requests)
  • Controllers that grow unmanageably large over time
  • Poor separation of concerns, making the codebase harder to maintain

Following Fat Model / Skinny Controller helps by:

  • Making controllers easier to read and maintain
  • Allowing business logic to be reused across controllers, background jobs, and console/rake tasks
  • Making logic easier to unit test in isolation
  • Encouraging better separation of concerns
  • Reducing duplication

How Does It Work?

The core idea is to ask: "Does this code belong to handling the HTTP request, or does it belong to the business rules of the application?" If it's the latter, it moves out of the controller.

Common destinations for extracted logic:

  • Models – Validations, scopes, associations, and logic tightly coupled to a single resource.
  • Service objects – Plain Ruby classes for logic that spans multiple models or represents a distinct business operation (e.g., OrderCheckout, InvoiceGenerator).
  • Concerns – Shared behavior reused across multiple models or controllers.
  • Query objects – Complex ActiveRecord queries extracted out of models when they grow too large.

Examples

Scenario 1: Fat Controller (Anti-pattern)

class OrdersController < ApplicationController 
  def create 
    @order = Order.new(order_params) 
    @order.total = @order.line_items.sum { |li| li.price * li.quantity } 
    @order.status = @order.total > 1000 ? "review" : "pending" 
 
    if @order.save 
      OrderMailer.confirmation(@order).deliver_later 
      redirect_to @order 
    else 
      render :new 
    end 
  end 
end 

Here, the controller is calculating totals and determining the order's status, both of which are business logic that doesn't belong in a controller.

Scenario 2: Moving Logic to the Model

class Order < ApplicationRecord 
  before_save :calculate_total, :set_status 
 
  private 
 
  def calculate_total 
    self.total = line_items.sum { |li| li.price * li.quantity } 
  end 
 
  def set_status 
    self.status = total > 1000 ? "review" : "pending" 
  end 
end 

 
class OrdersController < ApplicationController 
  def create 
    @order = Order.new(order_params) 
 
    if @order.save 
      OrderMailer.confirmation(@order).deliver_later 
      redirect_to @order 
    else 
      render :new 
    end 
  end 
end 

The controller is now skinny — it just handles the request/response cycle.

Scenario 3: Using a Service Object

When logic spans multiple models or represents a multi-step business process, a service object is often a better fit than the model itself:

class OrderCheckout 

  def initialize(order) 

    @order = order 

  end 

  

  def call 

    @order.calculate_total 

    @order.set_status 

  

    if @order.save 

      OrderMailer.confirmation(@order).deliver_later 

      true 

    else 

      false 

    end 

  end 

end 
class OrdersController < ApplicationController 

  def create 

    @order = Order.new(order_params) 

     

    if OrderCheckout.new(@order).call 

      redirect_to @order 

    else 

      render :new 

    end 

  end 

end

Where to Use This Pattern?

  • Controllers handling multi-step business processes (checkouts, signups, approvals)
  • Applications where the same business logic needs to run from controllers, background jobs, and rake tasks
  • Codebases where controller actions are growing long and difficult to follow
  • Any place where logic needs to be unit tested without simulating a full HTTP request

A Note of Caution

"Fat Model" doesn't mean unlimited model growth. Models can become equally bloated ("God models") if every piece of logic is dumped into them regardless of fit. Service objects, concerns, and query objects exist precisely to prevent this. The real goal is skinny controllers and well-organized business logic, not just relocating the problem.

In Summary

Fat Model / Skinny Controller is a Rails design philosophy that keeps controllers focused on handling requests while pushing business logic into models or service objects. This improves testability, reusability, and long-term maintainability of the codebase.