# Rails Patterns

> When to activate: Rails, MVC conventions, ActiveRecord associations, service objects, strong parameters, concerns, callbacks, form objects, Rails controllers and models

- Skill: `mattakushi432/rails-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/rails-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/rails-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/rails-patterns

---


# 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

```ruby
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

```ruby
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

```ruby
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

```ruby
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

```ruby
# 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

```ruby
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 on `has_many`/`has_one` to avoid orphaned records
- [ ] Multi-model forms use a form object instead of `accepts_nested_attributes_for` sprawl

## Anti-Patterns

```ruby
# 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.md`
- `skills/ruby-ecosystem/ruby-database.md`
- `skills/ruby-ecosystem/ruby-testing.md`

