This skill should be used when the user asks about ActiveRecord, database models, associations, validations, callbacks, scopes, queries, or business logic placement. Trigger phrases include "model design", "ActiveRecord", "associations", "has_many", "belongs_to", "validations", "callbacks", "scopes", "fat models", "where should this logic go", "database queries", "N+1 queries".
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
examples/associations.rbexamples/rich-model.rbreferences/business-logic-patterns.mdreferences/query-optimization.mdExpertise in ActiveRecord patterns, associations, validations, callbacks, and the "fat models" philosophy for Rails 8 applications.
Business logic belongs in models. The model is not just a database wrapper - it's the heart of your domain. If you're reaching for a service object, stop and ask: "Could this be a model method?"
User, LineItem, OrderStatus)users, line_items, order_statuses)singular_table_name_id (user_id, order_id)categories_products)Always use the generator:
bin/rails generate model Article title:string body:text published_at:datetime user:references
This creates the model, migration, and test files with proper conventions.
class User < ApplicationRecord
has_many :articles, dependent: :destroy
has_many :comments, through: :articles
has_one :profile, dependent: :destroy
end
class Article < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_and_belongs_to_many :categories
end
dependent: :destroy - Cascade deletes (use for owned relationships)dependent: :nullify - Set FK to null on deleteinverse_of: - Helps Rails recognize bidirectional associationscounter_cache: true - Maintain count for size calls without queriestouch: true - Update parent's updated_at on child changesFor attachable behavior (comments, images, etc.):
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Article < ApplicationRecord
has_many :comments, as: :commentable
end
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
class User < ApplicationRecord
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :username, presence: true,
length: { minimum: 3, maximum: 30 },
format: { with: /\A[a-zA-Z0-9_]+\z/ }
validates :age, numericality: { greater_than: 0 }, allow_nil: true
validates :terms, acceptance: true, on: :create
end
For domain-specific rules:
class Order < ApplicationRecord
validate :items_must_be_in_stock
private
def items_must_be_in_stock
line_items.each do |item|
unless item.product.in_stock?(item.quantity)
errors.add(:base, "#{item.product.name} is out of stock")
end
end
end
end
Use contexts for state-dependent validations:
class Article < ApplicationRecord
validates :body, presence: true, on: :publish
def publish!
update!(published_at: Time.current, context: :publish)
end
end
Order: before_validation → after_validation → before_save → before_create/update → after_create/update → after_save → after_commit
class User < ApplicationRecord
before_validation :normalize_email
after_create_commit :send_welcome_email
private
def normalize_email
self.email = email.downcase.strip if email.present?
end
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
end
DON'T use callbacks for:
before_* callbacks (use after_commit)class Article < ApplicationRecord
scope :published, -> { where.not(published_at: nil) }
scope :draft, -> { where(published_at: nil) }
scope :recent, -> { order(created_at: :desc) }
scope :by_author, ->(user) { where(user: user) }
scope :published_since, ->(date) { published.where(published_at: date..) }
end
# Chainable
Article.published.recent.by_author(current_user)
class Article < ApplicationRecord
default_scope { order(created_at: :desc) }
end
Warning: Default scopes apply everywhere, including associations. Often better to use explicit scopes.
# Good: Select only needed columns
User.select(:id, :name, :email).where(active: true)
# Good: Pluck for arrays of values
User.where(role: :admin).pluck(:email)
# Good: Batching for large datasets
User.find_each(batch_size: 1000) do |user|
user.send_newsletter
end
# Good: Exists check (doesn't load records)
User.exists?(email: params[:email])
# Bad: N+1
@articles = Article.all
@articles.each { |a| puts a.user.name } # Queries for each user
# Good: Eager loading
@articles = Article.includes(:user)
@articles.each { |a| puts a.user.name } # Single query for users
# Good: Preload specific associations
@articles = Article.includes(:user, :comments, :categories)
includes - Let Rails decide (usually preload)preload - Always separate queries (use with find_each)eager_load - Always LEFT OUTER JOINjoins - INNER JOIN (no association loading)Domain logic, state management, computed attributes:
class Order < ApplicationRecord
def total
line_items.sum(&:subtotal)
end
def complete!
transaction do
update!(status: :completed, completed_at: Time.current)
line_items.each(&:decrement_stock!)
OrderMailer.confirmation(self).deliver_later
end
end
def cancelable?
pending? && created_at > 24.hours.ago
end
end
The pattern of OrderCompletionService.new(order).call is unnecessary indirection. The order knows how to complete itself.
See references/business-logic-patterns.md for detailed guidance and examples/rich-model.rb for a complete example.
class User < ApplicationRecord
normalizes :email, with: ->(email) { email.strip.downcase }
normalizes :phone, with: ->(phone) { phone.gsub(/\D/, '') }
end
Cleaner than before_validation callbacks for data normalization.
class User < ApplicationRecord
generates_token_for :password_reset, expires_in: 15.minutes do
password_salt.last(10)
end
end
# Usage
token = user.generate_token_for(:password_reset)
User.find_by_token_for(:password_reset, token)
Configure in config/environments/development.rb:
config.active_record.strict_loading_by_default = true
Forces explicit eager loading, catching N+1 queries in development.
references/business-logic-patterns.md - Where logic belongsreferences/query-optimization.md - Performance patternsexamples/rich-model.rb - Complete model exampleexamples/associations.rb - Association patterns