Agent skill

active-record-model-best-practices

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/active-record-model-best-practices

SKILL.md

Rails Model Refactoring

When refactoring a Rails model, analyze the file and extract code into the appropriate pattern based on what the code does. The model itself should only contain associations, enums, basic validations, and concern includes.

Decision Framework

Read the model file and classify each block of code:

Code type Extract to Location
Related scopes + simple methods sharing a theme Concern app/models/concerns/
Business logic, multi-step operations, callbacks with side effects Service object app/services/
Complex queries, multi-join scopes, reporting queries Query object app/queries/
Context-specific validations (e.g. registration vs admin update) Form object app/forms/
Domain concepts beyond a primitive (money, coordinates, scores) Value object app/models/
Associations, enums, core validations, simple scopes Keep on model

Patterns

Concerns

Use for grouping related scopes, validations, callbacks, and simple instance methods that share a single theme. Name the concern after the capability it provides.

ruby
# app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern

  included do
    scope :search, ->(query) { where("name ILIKE ?", "%#{query}%") }
  end

  def matching_terms(query)
    name.scan(/#{Regexp.escape(query)}/i)
  end
end

Service Objects

Use for business logic, orchestration of multiple models, and anything triggered by a user action that involves more than a simple CRUD operation. Follow the single-responsibility principle — one service, one operation.

ruby
# app/services/players/calculate_stats.rb
module Players
  class CalculateStats
    def initialize(player)
      @player = player
    end

    def call
      # complex logic here
    end
  end
end

Conventions:

  • Namespace under the model name: Players::CalculateStats
  • Single public method: call
  • Accept dependencies via initialize
  • Return a result or raise a domain-specific error

Query Objects

Use for complex database queries that involve joins, subqueries, CTEs, or multi-condition filtering that would clutter a model with scopes.

ruby
# app/queries/players/free_agent_query.rb
module Players
  class FreeAgentQuery
    def initialize(relation = Player.all)
      @relation = relation
    end

    def call(filters = {})
      @relation
        .where(contract_status: :expired)
        .where("age < ?", filters[:max_age])
        .joins(:stats)
        .order(war: :desc)
    end
  end
end

Conventions:

  • Accept a base relation in initialize (default to Model.all)
  • Return an ActiveRecord relation so it remains chainable
  • Single public method: call

Form Objects

Use when validations only apply in specific contexts, or when a form spans multiple models.

ruby
# app/forms/player_registration_form.rb
class PlayerRegistrationForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :email, :string
  attribute :team_id, :integer
  attribute :position, :string

  validates :name, :email, :position, presence: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }

  def save
    return false unless valid?
    Player.create!(attributes)
  end
end

Value Objects

Use for domain concepts that deserve their own identity beyond a raw primitive.

ruby
# app/models/batting_average.rb
class BattingAverage
  include Comparable

  def initialize(hits, at_bats)
    @hits = hits
    @at_bats = at_bats
  end

  def value
    return 0.0 if @at_bats.zero?
    (@hits.to_f / @at_bats).round(3)
  end

  def elite?
    value >= 0.300
  end

  def <=>(other)
    value <=> other.value
  end
end

Refactoring Process

  1. Read the entire model and identify every method, scope, callback, and validation.
  2. Classify each block using the decision framework table above.
  3. Extract in order: value objects first, then query objects, then service objects, then concerns. Do concerns last because some may become unnecessary after other extractions.
  4. Update the model to include concerns and delegate to new objects.
  5. Verify that the slimmed-down model only contains: associations, enums, core validations, and concern includes.
  6. Create or update tests for each extracted class. Each new class gets its own spec file mirroring the source path.

What NOT to Do

  • Don't extract everything — simple one-line scopes and basic validations belong on the model.
  • Don't create a class for trivial logic just to hit a line count target.
  • Don't use concerns as junk drawers — each concern should have a clear, single theme.
  • Don't break ActiveRecord conventions (e.g. don't move associations into concerns).
  • Don't introduce callback-heavy service objects — prefer explicit invocation over implicit hooks.

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results