RL ROLAND LOPEZ
// 2 min read

Refactoring Fat Models in Rails: Delegate, Extract, and Compose

Refactoring Fat Models in Rails: Delegate, Extract, and Compose β€” Reduce complexity in Rails models with method delegation, extraction into service classes, and composed_of - keeping ActiveRecord lean and SRP-aligned.

Introduction

Reduce complexity in Rails models with method delegation, extraction, and composition.

Initial fat model example

Added comments to explain how we could group these methods.

# app/models/order.rb
class Order < ActiveRecord::Base
  # Various state finders
  def find_purchased
  end

  def find_waiting_for_review
  end

  def find_waiting_for_sign_off
  end

  # Search methods
  def advanced_search(fields, options = {})
  end

  def simple_search(terms)
  end

  # Export methods
  def to_xml
  end

  def to_json
  end

  def to_csv
  end

  def to_pdf
  end
end

This model is overwhelmingly cumbersome and violates the Single Responsibility Principle by handling multiple aspects of the order process.

Refactoring using delegate

First, extract the conversion methods into a separate class and use Rails’ delegation mechanism to keep the interface of the Order model clean.

Creating the new OrderConverter class

The responsibility of this class is to convert the model into XML, JSON, CSV, and PDF, as the initial model was doing.

# app/models/order_converter.rb
# you can also move this to /lib or /concerns
class OrderConverter
  attr_reader :order

  def initialize(order)
    @order = order
  end

  def to_xml
  end

  def to_json
  end

  def to_csv
  end

  def to_pdf
  end
end

Updated Order model with delegation

# app/models/order.rb
class Order < ActiveRecord::Base
  delegate :to_xml, :to_json, :to_csv, :to_pdf, to: :converter

  def converter
    OrderConverter.new(self)
  end
end

With this setup, calls like @order.to_pdf seamlessly forward to OrderConverter, adhering to both the Single Responsibility Principle and the Law of Demeter.

Extracting logic into a separate class

Another common refactor is moving related functionality into service or value classes, which can then be composed back into the main model.

Example: a complex BankAccount model

# app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
  validates :balance_in_cents, presence: true
  validates :currency, presence: true

  def balance_in_other_currency(currency)
    # Logic to convert in other currencies
  end

  def balance
    balance_in_cents / 100
  end

  def balance_equal?(other_bank_account)
    balance_in_cents == other_bank_account.balance_in_other_currency(currency)
  end
end

This model improperly handles diverse responsibilities.

Using composed_of for money management

By introducing a Money class and linking it via composed_of, BankAccount responsibilities are clearly divided.

# app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
  validates :balance_in_cents, presence: true
  validates :currency, presence: true

  composed_of :balance,
            class_name: "Money",
            mapping: [%w(balance_in_cents amount_in_cents),
                      %w(currency currency)]
end
# app/models/money.rb
class Money
  include Comparable
  attr_accessor :amount_in_cents, :currency

  def initialize(amount_in_cents, currency)
    self.amount_in_cents = amount_in_cents
    self.currency = currency
  end

  def in_currency(other_currency)
    # Logic to convert in other currencies
  end

  def amount
    amount_in_cents / 100
  end

  def <=>(other_money)
    amount_in_cents <=> other_money.in_currency(currency).amount_in_cents
  end
end

Controller usage

@bank_account.balance.in_currency(:eur)

This keeps the BankAccount model lean and delegates monetary calculations to the Money class.

Likewise, you can compare balances with @bank_account.balance > @other_bank_account.balance regardless of currencies.

Conclusion

Refactoring fat models in Ruby on Rails through meticulous extraction and composition leads to cleaner, more maintainable codebases.

This approach not only aligns with SOLID principles but also enhances the flexibility and robustness of the application architecture.

Roland Lopez
Written by
Roland Lopez

Technical co-founder specialized in SaaS, DevOps, AI agents, and data platforms. Building and scaling with Ruby on Rails, n8n, and fast feedback loops.

Built by Agent Skynet See the agency