Rails Patterns
When to Use
Building or reviewing a Rails application: controllers, ActiveRecord models, forms spanning multiple models, or deciding where business logic should live.
Core Patterns
Strong Parameters
class OrdersController < ApplicationController
def create
order = current_user.orders.create!(order_params)
render json: order, status: :created
end
private
def order_params
params.require(:order).permit(:customer_id, items: [:sku, :qty])
end
end
Never call params.permit! (permits everything) or pass raw params to create/update.
Service Objects for Multi-Step Business Logic
class CreateOrder
def initialize(customer:, items:)
@customer = customer
@items = items
end
def call
ActiveRecord::Base.transaction do
order = Order.create!(customer: @customer)
@items.each { |item| reserve_inventory(order, item) }
order
end
end
private
def reserve_inventory(order, item)
product = Product.lock.find(item[:product_id])
product.decrement!(:stock, item[:qty])
order.order_items.create!(product:, qty: item[:qty])
end
end
# Controller
order = CreateOrder.new(customer: current_user, items: params[:items]).call
Keeps controllers thin and models focused on persistence, not orchestration.
Associations, Explicitly Scoped
class Order < ApplicationRecord
belongs_to :customer
has_many :order_items, dependent: :destroy
has_many :products, through: :order_items
scope :pending, -> { where(status: :pending) }
scope :for_customer, ->(customer) { where(customer:) }
end
Order.pending.for_customer(current_user)
Concerns: Extract Shared Behavior, Not a Dumping Ground
module Archivable
extend ActiveSupport::Concern
included do
scope :archived, -> { where.not(archived_at: nil) }
scope :active, -> { where(archived_at: nil) }
end
def archive!
update!(archived_at: Time.current)
end
end
class Order < ApplicationRecord
include Archivable
end
Use a concern only when the same behavior is genuinely shared across multiple models — not as a place to hide an oversized model's methods under a different filename.
Callbacks: Prefer Explicit Calls
# RISKY: implicit side effect, easy to miss when reading OrderController
class Order < ApplicationRecord
after_create :send_confirmation_email
end
# CLEARER: explicit at the call site, easier to trace and test in isolation
class CreateOrder
def call
order = Order.create!(order_params)
OrderMailer.confirmation(order).deliver_later
order
end
end
Callbacks are fine for genuinely intrinsic model behavior (e.g. normalizing an email before save). Avoid them for cross-cutting side effects like sending emails or calling external services — those belong in the orchestrating service object where they're visible and independently testable.
Form Objects for Multi-Model Forms
class SignupForm
include ActiveModel::Model
attr_accessor :name, :email, :company_name
validates :name, :email, :company_name, presence: true
validates :email, format: URI::MailTo::EMAIL_REGEXP
def save
return false unless valid?
ActiveRecord::Base.transaction do
company = Company.create!(name: company_name)
User.create!(name:, email:, company:)
end
true
end
end
Checklist
- Strong parameters used on every create/update action; no
params.permit! - Multi-step business logic in service objects, not spread across model callbacks
- Concerns hold genuinely shared behavior, not just a way to split a large model file
- Cross-cutting side effects (email, external API calls) triggered explicitly, not buried in
after_save -
dependent:specified onhas_many/has_oneto avoid orphaned records - Multi-model forms use a form object instead of
accepts_nested_attributes_forsprawl
Anti-Patterns
# BAD: fat model doing everything — persistence, business rules, notifications
class Order < ApplicationRecord
after_create :charge_payment, :send_email, :update_inventory, :notify_warehouse
end
# GOOD: model stays persistence-focused; a service object orchestrates the workflow
Quick Reference
| Concern | Rails Tool |
|---|---|
| Input filtering | Strong Parameters |
| Multi-step business logic | Service Object (PORO) |
| Shared model behavior | ActiveSupport::Concern |
| Multi-model form | Form Object (ActiveModel::Model) |
| Reused query filters | Named scopes |
| Background work | ActiveJob / Sidekiq |
See Also
skills/ruby-ecosystem/ruby-patterns.mdskills/ruby-ecosystem/ruby-database.mdskills/ruby-ecosystem/ruby-testing.md