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.