rails conventions & patterns

Rails Conventions & Patterns Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rails conventions & patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-rails-conventions-patterns

Rails Conventions & Patterns Skill

This skill provides authoritative guidance on Ruby on Rails conventions, design patterns, and idiomatic code standards for production applications.

When to Use This Skill

  • Writing new Rails controllers, models, or services

  • Refactoring existing Rails code

  • Making decisions about code organization

  • Choosing between different Rails patterns

  • Ensuring code follows Rails conventions

  • Reviewing Rails code for convention compliance

Ruby & Rails Versions

ruby: "3.2+ (prefer 3.3+ for YJIT benefits)" rails: "7.1+ (prefer 8.0+ for new projects)"

Rails 7.x/8.x Modern Features

Rails 7.1+ Features

Composite Primary Keys

class BookOrder < ApplicationRecord self.primary_key = [:shop_id, :id] belongs_to :shop has_many :line_items, foreign_key: [:shop_id, :order_id] end

ActiveRecord::Encryption (sensitive data)

class User < ApplicationRecord encrypts :email, deterministic: true encrypts :ssn, :credit_card end

Horizontal Sharding

class ApplicationRecord < ActiveRecord::Base connects_to shards: { default: { writing: :primary, reading: :primary_replica }, shard_two: { writing: :primary_shard_two } } end

Async Query Loading

posts = Post.where(published: true).load_async

Do other work

posts.to_a # Wait for results

Normalize values before validation

class User < ApplicationRecord normalizes :email, with: -> { _1.strip.downcase } normalizes :phone, with: -> { _1.gsub(/\D/, '') } end

Rails 8.0+ Features

Improved Solid Queue (built-in job backend)

config/application.rb

config.active_job.queue_adapter = :solid_queue

Solid Cache (built-in caching)

config/application.rb

config.cache_store = :solid_cache_store

Authentication generator

rails generate authentication

Built-in rate limiting

class Api::PostsController < Api::BaseController rate_limit to: 10, within: 1.minute, only: :create end

Per-environment credentials

rails credentials:edit --environment production

Modern Ruby 3.3+ Features

Pattern matching in case expressions

case user in { role: "admin", active: true } grant_full_access in { role: "user", active: true } grant_standard_access else deny_access end

Endless method definitions (one-liners)

def full_name = "#{first_name} #{last_name}" def published? = published_at.present?

Data class (immutable value objects, Ruby 3.2+)

User = Data.define(:id, :name, :email) user = User.new(id: 1, name: "Alice", email: "alice@example.com")

YJIT optimization (Ruby 3.3+)

config/application.rb

if defined?(RubyVM::YJIT.enable) RubyVM::YJIT.enable end

File Organization Standards

Models

location: "app/models/" max_lines: 200 guidance: | Focus on associations, validations, scopes, and essential callbacks. Extract business logic to Service Objects. Keep models focused on data persistence and domain rules.

Controllers

location: "app/controllers/" max_lines: 100 guidance: | Limit to REST actions. Use before_action for shared logic. Complex operations delegate to Service Objects. Follow "Skinny Controller, Fat Model (but not too fat)" pattern.

Comprehensive Controller Patterns

RESTful Controller Structure

class PostsController < ApplicationController before_action :authenticate_user! before_action :set_post, only: [:show, :edit, :update, :destroy] before_action :authorize_post, only: [:edit, :update, :destroy]

GET /posts

def index @posts = Post.published.page(params[:page]) end

GET /posts/:id

def show # @post set by before_action end

GET /posts/new

def new @post = Post.new end

POST /posts

def create @post = CreatePostService.call(current_user, post_params)

if @post.persisted?
  redirect_to @post, notice: "Post created successfully"
else
  render :new, status: :unprocessable_entity
end

end

GET /posts/:id/edit

def edit # @post set by before_action end

PATCH /posts/:id

def update if UpdatePostService.call(@post, post_params) redirect_to @post, notice: "Post updated successfully" else render :edit, status: :unprocessable_entity end end

DELETE /posts/:id

def destroy @post.destroy! redirect_to posts_url, notice: "Post deleted successfully" end

private

def set_post @post = Post.find(params[:id]) end

def authorize_post authorize @post # Pundit end

def post_params params.require(:post).permit(:title, :body, :published) end end

API Controller Patterns

app/controllers/api/base_controller.rb

module Api class BaseController < ActionController::API include ActionController::HttpAuthentication::Token::ControllerMethods

before_action :authenticate_api_user!

rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from Pundit::NotAuthorizedError, with: :forbidden

private

def authenticate_api_user!
  authenticate_or_request_with_http_token do |token, options|
    @current_user = User.find_by(api_token: token)
  end
end

def not_found(exception)
  render json: { error: exception.message }, status: :not_found
end

def unprocessable_entity(exception)
  render json: { errors: exception.record.errors }, status: :unprocessable_entity
end

def forbidden
  render json: { error: "Forbidden" }, status: :forbidden
end

end end

app/controllers/api/v1/posts_controller.rb

module Api module V1 class PostsController < Api::BaseController def index posts = Post.published.page(params[:page]) render json: PostBlueprint.render(posts, root: :posts) end

  def create
    post = CreatePostService.call(current_user, post_params)

    if post.persisted?
      render json: PostBlueprint.render(post), status: :created
    else
      render json: { errors: post.errors }, status: :unprocessable_entity
    end
  end
end

end end

Hotwire Controller Patterns

class PostsController < ApplicationController

Turbo Stream responses

def create @post = CreatePostService.call(current_user, post_params)

respond_to do |format|
  if @post.persisted?
    format.turbo_stream
    format.html { redirect_to @post }
  else
    format.turbo_stream { render :form_errors, status: :unprocessable_entity }
    format.html { render :new, status: :unprocessable_entity }
  end
end

end

def update respond_to do |format| if UpdatePostService.call(@post, post_params) format.turbo_stream format.html { redirect_to @post } else format.turbo_stream { render :form_errors, status: :unprocessable_entity } format.html { render :edit, status: :unprocessable_entity } end end end end

app/views/posts/create.turbo_stream.erb

<%= turbo_stream.prepend "posts", @post %> <%= turbo_stream.update "new_post_form", "" %>

Nested Resource Controllers

class CommentsController < ApplicationController before_action :set_post before_action :set_comment, only: [:show, :edit, :update, :destroy]

GET /posts/:post_id/comments

def index @comments = @post.comments.page(params[:page]) end

POST /posts/:post_id/comments

def create @comment = @post.comments.build(comment_params) @comment.user = current_user

if @comment.save
  redirect_to [@post, @comment]
else
  render :new, status: :unprocessable_entity
end

end

private

def set_post @post = Post.find(params[:post_id]) end

def set_comment @comment = @post.comments.find(params[:id]) end end

Controller Concerns

app/controllers/concerns/paginatable.rb

module Paginatable extend ActiveSupport::Concern

included do before_action :set_pagination_params end

private

def set_pagination_params @page = params[:page] || 1 @per_page = params[:per_page] || 25 end

def paginate(collection) collection.page(@page).per(@per_page) end end

Usage

class PostsController < ApplicationController include Paginatable

def index @posts = paginate(Post.published) end end

Services

location: "app/services/" naming: "{Domain}Manager::{Action} (e.g., OrdersManager::CreateOrder)" structure: | class OrdersManager::CreateOrder def initialize(user:, params:) @user = user @params = params end

def call
  # Single public entry point
  # Returns Result object or raises
end

private

# Small, focused private methods

end

Methods

max_lines: 15 max_params: 4 guidance: "If method needs more params, use a Parameter Object or Hash"

Naming Conventions

classes: "PascalCase" methods: "snake_case" predicates: "end with ? (e.g., active?, valid?)" dangerous_methods: "end with ! (e.g., save!, destroy!)" constants: "SCREAMING_SNAKE_CASE" private_methods: "Prefix with purpose, not underscore"

Ruby Idioms

Prefer

  • Guard clauses over nested conditionals

  • Explicit returns for clarity

  • &. (safe navigation) over try

  • Keyword arguments for 2+ parameters

  • Struct /Data for simple value objects

  • frozen_string_literal: true pragma

Avoid

  • unless with else

  • Nested ternaries

  • and /or for control flow

  • Monkey patching in application code

Pattern Decision Tree

Always inspect existing codebase patterns before recommending any pattern.

Service Object

Use when:

- Business logic spans multiple models

- Operation has multiple steps

- Logic doesn't belong to any single model

- Need to orchestrate external services

Avoid when:

- Simple CRUD operation

- Logic clearly belongs to one model

- Single-line delegation

Inspect first:

ls app/services/

Check existing service naming convention

Form Object

Use when:

- Form spans multiple models

- Complex validations not tied to persistence

- Wizard/multi-step forms

Avoid when:

- Standard single-model form

- Simple attribute updates

Inspect first:

ls app/forms/ 2>/dev/null

grep -r 'include ActiveModel' app/ --include='*.rb'

Query Object

Use when:

- Complex queries with multiple conditions

- Query logic reused across controllers

- Query needs composition/chaining

Avoid when:

- Simple scope suffices

- One-off query

Inspect first:

ls app/queries/ 2>/dev/null

grep -r 'class.Query' app/ --include='.rb'

Concern

Use when:

- Truly shared behavior across 3+ unrelated models

- Behavior is cohesive and self-contained

Avoid when:

- Only 1-2 models share the code

- Behavior is not cohesive

- Just to 'clean up' a model

Inspect first:

ls app/models/concerns/ app/controllers/concerns/

Check how many models use each concern

Decorator/Presenter

Use when:

- View logic becoming complex

- Same presentation logic in multiple views

- Need to augment model for display

Avoid when:

- Simple attribute display

- One-off formatting

Inspect first:

ls app/decorators/ app/presenters/ 2>/dev/null

grep 'draper' Gemfile

ActionMailer Conventions

Mailer Structure

app/mailers/user_mailer.rb

class UserMailer < ApplicationMailer default from: 'notifications@example.com'

def welcome_email(user) @user = user @url = root_url

mail(
  to: email_address_with_name(@user.email, @user.name),
  subject: 'Welcome to My App'
)

end

def password_reset(user, token) @user = user @token = token @reset_url = edit_password_reset_url(token: @token)

mail(to: @user.email, subject: 'Password Reset Instructions')

end

private

def email_address_with_name(email, name) Mail::Address.new(email).tap { |a| a.display_name = name }.format end end

app/views/user_mailer/welcome_email.html.erb

<h1>Welcome <%= @user.name %>!</h1> <p>Click here to get started: <%= link_to 'Get Started', @url %></p>

app/views/user_mailer/welcome_email.text.erb

Welcome <%= @user.name %>!

Click here to get started: <%= @url %>

Mailer Testing

spec/mailers/user_mailer_spec.rb

RSpec.describe UserMailer, type: :mailer do describe '#welcome_email' do let(:user) { create(:user, email: 'user@example.com') } let(:mail) { UserMailer.welcome_email(user) }

it 'renders the subject' do
  expect(mail.subject).to eq('Welcome to My App')
end

it 'renders the receiver email' do
  expect(mail.to).to eq([user.email])
end

it 'renders the sender email' do
  expect(mail.from).to eq(['notifications@example.com'])
end

it 'contains user name' do
  expect(mail.body.encoded).to match(user.name)
end

end end

Mailer Previews (Rails 4.1+)

test/mailers/previews/user_mailer_preview.rb

class UserMailerPreview < ActionMailer::Preview def welcome_email UserMailer.welcome_email(User.first) end

def password_reset user = User.first token = SecureRandom.urlsafe_base64 UserMailer.password_reset(user, token) end end

Visit: http://localhost:3000/rails/mailers/user_mailer/welcome_email

Background Delivery

Deliver later (asynchronous)

UserMailer.welcome_email(@user).deliver_later

Deliver later with delay

UserMailer.welcome_email(@user).deliver_later(wait: 1.hour)

Deliver later at specific time

UserMailer.welcome_email(@user).deliver_later(wait_until: Date.tomorrow.noon)

Deliver now (synchronous)

UserMailer.welcome_email(@user).deliver_now

Background Job Conventions

ActiveJob Structure

app/jobs/application_job.rb

class ApplicationJob < ActiveJob::Base

Global retry configuration

retry_on StandardError, wait: :exponentially_longer, attempts: 5 retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3

Discard specific errors

discard_on ActiveJob::DeserializationError

Global error handling

rescue_from(Exception) do |exception| ErrorTracker.notify(exception) raise exception end end

app/jobs/send_welcome_email_job.rb

class SendWelcomeEmailJob < ApplicationJob queue_as :mailers

def perform(user) UserMailer.welcome_email(user).deliver_now end end

Usage

SendWelcomeEmailJob.perform_later(user)

Sidekiq-Specific Patterns

app/jobs/process_order_job.rb

class ProcessOrderJob < ApplicationJob queue_as :orders

Sidekiq-specific options

sidekiq_options retry: 3, backtrace: true, dead: true

def perform(order_id) order = Order.find(order_id) OrderProcessor.new(order).process! end end

config/sidekiq.yml

:queues:

  • critical
  • default
  • mailers
  • low_priority

:schedule: daily_cleanup: cron: '0 0 * * *' # Daily at midnight class: DailyCleanupJob

Job Testing

spec/jobs/send_welcome_email_job_spec.rb

RSpec.describe SendWelcomeEmailJob, type: :job do include ActiveJob::TestHelper

let(:user) { create(:user) }

it 'enqueues the job' do expect { SendWelcomeEmailJob.perform_later(user) }.to have_enqueued_job(SendWelcomeEmailJob).with(user) end

it 'sends welcome email' do expect { perform_enqueued_jobs do SendWelcomeEmailJob.perform_later(user) end }.to change { ActionMailer::Base.deliveries.count }.by(1) end

it 'retries on failure' do allow(UserMailer).to receive(:welcome_email).and_raise(StandardError)

expect {
  SendWelcomeEmailJob.perform_later(user)
}.to have_enqueued_job(SendWelcomeEmailJob).on_queue(:mailers)

end end

Action Cable (WebSocket) Conventions

Channel Structure

app/channels/application_cable/connection.rb

module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user

def connect
  self.current_user = find_verified_user
end

private

def find_verified_user
  if verified_user = User.find_by(id: cookies.encrypted[:user_id])
    verified_user
  else
    reject_unauthorized_connection
  end
end

end end

app/channels/chat_channel.rb

class ChatChannel < ApplicationCable::Channel def subscribed # Stream from specific room stream_from "chat_#{params[:room_id]}"

# Or stream for current user
stream_for current_user

end

def unsubscribed # Cleanup when channel is unsubscribed stop_all_streams end

def speak(data) # Receive data from client message = current_user.messages.create!( content: data['message'], room_id: params[:room_id] )

# Broadcast to all subscribers
ActionCable.server.broadcast(
  "chat_#{params[:room_id]}",
  message: render_message(message)
)

end

private

def render_message(message) ApplicationController.render( partial: 'messages/message', locals: { message: message } ) end end

Client-Side JavaScript

// app/javascript/channels/chat_channel.js import consumer from "./consumer"

consumer.subscriptions.create( { channel: "ChatChannel", room_id: roomId }, { connected() { console.log("Connected to chat") },

disconnected() {
  console.log("Disconnected from chat")
},

received(data) {
  const messages = document.getElementById('messages')
  messages.insertAdjacentHTML('beforeend', data.message)
},

speak(message) {
  this.perform('speak', { message: message })
}

} )

Broadcasting from Models

app/models/message.rb

class Message < ApplicationRecord belongs_to :user belongs_to :room

after_create_commit :broadcast_message

private

def broadcast_message broadcast_append_to( [room, :messages], target: "messages", partial: "messages/message", locals: { message: self } ) end end

Cable Testing

spec/channels/chat_channel_spec.rb

RSpec.describe ChatChannel, type: :channel do let(:user) { create(:user) } let(:room) { create(:room) }

before do stub_connection(current_user: user) end

it 'successfully subscribes' do subscribe(room_id: room.id) expect(subscription).to be_confirmed expect(subscription).to have_stream_from("chat_#{room.id}") end

it 'broadcasts messages' do subscribe(room_id: room.id)

expect {
  perform :speak, message: 'Hello'
}.to have_broadcasted_to("chat_#{room.id}")

end end

Enhanced Concern Best Practices

When to Use Concerns

GOOD: Truly shared behavior across unrelated models

app/models/concerns/publishable.rb

module Publishable extend ActiveSupport::Concern

included do scope :published, -> { where(published: true) } scope :draft, -> { where(published: false) }

validates :published_at, presence: true, if: :published?

end

def publish! update!(published: true, published_at: Time.current) end

def unpublish! update!(published: false, published_at: nil) end end

Used in multiple unrelated models

class Post < ApplicationRecord include Publishable end

class Video < ApplicationRecord include Publishable end

class Podcast < ApplicationRecord include Publishable end

Concern with Dependencies

app/models/concerns/taggable.rb

module Taggable extend ActiveSupport::Concern

included do # Dependencies injection has_many :taggings, as: :taggable, dependent: :destroy has_many :tags, through: :taggings

scope :tagged_with, ->(tag_name) {
  joins(:tags).where(tags: { name: tag_name })
}

end

Instance methods

def tag_names=(names) self.tags = names.map { |n| Tag.find_or_create_by(name: n.strip) } end

def tag_names tags.pluck(:name) end

Class methods

class_methods do def most_tagged(limit = 10) select('taggable_id, COUNT(*) as tags_count') .group('taggable_id') .order('tags_count DESC') .limit(limit) end end end

Controller Concerns

app/controllers/concerns/error_handling.rb

module ErrorHandling extend ActiveSupport::Concern

included do rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity rescue_from Pundit::NotAuthorizedError, with: :unauthorized end

private

def not_found respond_to do |format| format.html { render 'errors/404', status: :not_found } format.json { render json: { error: 'Not found' }, status: :not_found } end end

def unprocessable_entity(exception) respond_to do |format| format.html { render 'errors/422', status: :unprocessable_entity } format.json { render json: { errors: exception.record.errors }, status: :unprocessable_entity } end end

def unauthorized respond_to do |format| format.html { redirect_to root_path, alert: 'Not authorized' } format.json { render json: { error: 'Not authorized' }, status: :forbidden } end end end

Usage

class ApplicationController < ActionController::Base include ErrorHandling end

Method Visibility Rules

Public

Callable from anywhere, defines the API

Controller actions must be public

Methods called from views must be public

Service interface methods

Rails context:

- Controller: only public methods are routable

- Model: public methods accessible from controllers/views

- Component: only public methods callable from templates

Private

Can only be called within the class, without explicit receiver

Implementation details

Helper methods not part of public API

Methods that should never be called externally

Rails context:

- Controller: helper methods, before_action callbacks

- Service: internal computation methods

- Model: internal validation helpers

CRITICAL: Private methods CANNOT be called from outside the class.

If a view needs data, the component MUST have a public method.

Protected

Callable from same class or subclasses

Methods meant for inheritance

Rare in typical Rails apps

Rails context:

- Occasionally in base controllers/models for shared behavior

Delegation Patterns

Using delegate

Creates public forwarding methods

LIMITATION: Cannot delegate to private methods on target

delegate :method1, :method2, to: :target

class Component < ViewComponent::Base delegate :total, :count, to: :@service

def initialize(service:) @service = service end end

Now view can call component.total

Wrapper Methods

Use when:

- Need to transform data

- Need to add caching

- Need different method names

- Need to handle errors

class Component < ViewComponent::Base def total @service.calculate_total rescue ServiceError 0 end end

attr_reader Exposure

Expose the underlying object directly

Use sparingly - breaks encapsulation

class Component < ViewComponent::Base attr_reader :service

def initialize(service:) @service = service end end

View calls: component.service.calculate_total

Rails Request Cycle

Request → Route → Controller#action → Controller → Service/Model (business logic) → Controller → sets @instance_variables → Controller → renders View → View → calls methods on @variables → View → renders Components → Component → accesses only its own methods

Key Insight: Each layer can only access what the previous layer explicitly provides. Views can't magically access service internals.

Implementation Order

Always implement in dependency order (bottom-up):

  1. Database migrations (if needed)
  2. Models (foundation)
  3. Services (business logic)
  4. Components (presentation wrappers)
  5. Controllers (orchestration)
  6. Views (final layer)
  7. Tests (verify everything works)

Rationale: Each layer depends on the ones below it. Implementing bottom-up ensures dependencies exist before they're used.

Code Quality Standards

Method Size

  • Maximum 15 lines per method

  • Single responsibility per method

  • Extract complex logic to private helper methods

Class Size

  • Models: max 200 lines

  • Controllers: max 100 lines

  • Services: max 150 lines

Parameter Count

  • Maximum 4 parameters

  • Use keyword arguments for 2+ parameters

  • Use Parameter Objects for complex cases

Form Objects (Expanded)

Basic Form Object

app/forms/user_registration_form.rb

class UserRegistrationForm include ActiveModel::Model include ActiveModel::Attributes

attribute :email, :string attribute :password, :string attribute :password_confirmation, :string attribute :first_name, :string attribute :last_name, :string attribute :accept_terms, :boolean

validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } validates :password, presence: true, length: { minimum: 8 } validates :password_confirmation, presence: true validates :first_name, :last_name, presence: true validates :accept_terms, acceptance: true validate :passwords_match

def save return false unless valid?

ActiveRecord::Base.transaction do
  @user = User.create!(
    email: email,
    password: password,
    first_name: first_name,
    last_name: last_name
  )

  @profile = @user.create_profile!(
    full_name: "#{first_name} #{last_name}"
  )

  SendWelcomeEmailJob.perform_later(@user)
end

true

rescue ActiveRecord::RecordInvalid => e errors.add(:base, e.message) false end

attr_reader :user, :profile

private

def passwords_match return if password == password_confirmation

errors.add(:password_confirmation, "doesn't match password")

end end

Controller usage

def create @form = UserRegistrationForm.new(registration_params)

if @form.save redirect_to @form.user, notice: 'Registration successful' else render :new, status: :unprocessable_entity end end

Multi-Step Wizard Form

app/forms/checkout_wizard.rb

class CheckoutWizard include ActiveModel::Model

STEPS = [:shipping, :payment, :confirmation].freeze

attr_accessor :current_step attr_reader :order

delegate :shipping_address, :billing_address, :payment_method, :shipping_address=, :billing_address=, :payment_method=, to: :order

validates :shipping_address, presence: true, if: :shipping_step? validates :payment_method, presence: true, if: :payment_step?

def initialize(order, current_step: :shipping) @order = order @current_step = current_step.to_sym end

def next_step return if last_step?

self.current_step = STEPS[STEPS.index(current_step) + 1]

end

def previous_step return if first_step?

self.current_step = STEPS[STEPS.index(current_step) - 1]

end

def save return false unless valid?

order.save

end

def first_step? current_step == STEPS.first end

def last_step? current_step == STEPS.last end

private

def shipping_step? current_step == :shipping end

def payment_step? current_step == :payment end end

Decorators (Expanded)

Draper Decorator Pattern

Gemfile

gem 'draper'

app/decorators/application_decorator.rb

class ApplicationDecorator < Draper::Decorator delegate_all

def created_at h.content_tag(:time, object.created_at.strftime("%B %d, %Y"), datetime: object.created_at.iso8601) end end

app/decorators/user_decorator.rb

class UserDecorator < ApplicationDecorator def full_name "#{object.first_name} #{object.last_name}" end

def profile_link h.link_to full_name, h.user_path(object), class: 'user-link' end

def avatar if object.avatar.attached? h.image_tag object.avatar.variant(resize_to_limit: [100, 100]) else h.image_tag 'default-avatar.png', alt: full_name end end

def status_badge css_class = object.active? ? 'badge-success' : 'badge-secondary' status_text = object.active? ? 'Active' : 'Inactive'

h.content_tag(:span, status_text, class: "badge #{css_class}")

end

def member_since "Member since #{object.created_at.strftime('%B %Y')}" end end

Controller usage

def show @user = User.find(params[:id]).decorate end

View usage

<%= @user.profile_link %> <%= @user.avatar %> <%= @user.status_badge %>

SimpleDelegator Pattern (Without Gems)

app/decorators/user_decorator.rb

class UserDecorator < SimpleDelegator def initialize(user, view_context) super(user) @view_context = view_context end

def full_name "#{first_name} #{last_name}" end

def profile_link h.link_to full_name, h.user_path(self) end

def formatted_created_at created_at.strftime("%B %d, %Y") end

private

def h @view_context end end

Controller

def show user = User.find(params[:id]) @user = UserDecorator.new(user, view_context) end

Presenters (Expanded)

View-Specific Presenter

app/presenters/dashboard_presenter.rb

class DashboardPresenter def initialize(user, view_context) @user = user @view_context = view_context end

def welcome_message time_of_day = Time.current.hour < 12 ? 'Morning' : 'Afternoon' "Good #{time_of_day}, #{@user.first_name}!" end

def recent_orders @recent_orders ||= @user.orders.recent.limit(5).map do |order| OrderPresenter.new(order, @view_context) end end

def total_spent h.number_to_currency(@user.orders.sum(:total)) end

def activity_feed @user.activities.recent.limit(10).map do |activity| { icon: activity_icon(activity), text: activity_text(activity), time: h.time_ago_in_words(activity.created_at) } end end

def stats { total_orders: @user.orders.count, total_spent: total_spent, favorite_category: @user.favorite_category&.name || 'N/A', member_since: @user.created_at.year } end

private

def h @view_context end

def activity_icon(activity) case activity.action when 'order_placed' then 'shopping-cart' when 'review_posted' then 'star' when 'profile_updated' then 'user' else 'activity' end end

def activity_text(activity) case activity.action when 'order_placed' "You placed order ##{activity.target_id}" when 'review_posted' "You reviewed #{activity.target.product.name}" when 'profile_updated' "You updated your profile" end end end

Controller

def dashboard @presenter = DashboardPresenter.new(current_user, view_context) end

View

<h1><%= @presenter.welcome_message %></h1>

<div class="stats"> <% @presenter.stats.each do |key, value| %> <div class="stat"> <span class="label"><%= key.to_s.humanize %></span> <span class="value"><%= value %></span> </div> <% end %> </div>

Collection Presenter

app/presenters/users_index_presenter.rb

class UsersIndexPresenter def initialize(users, view_context, filters: {}) @users = users @view_context = view_context @filters = filters end

def users @decorated_users ||= @users.map { |u| UserDecorator.new(u, h) } end

def total_count @users.total_count end

def pagination h.paginate(@users) end

def active_filters @filters.select { |_, v| v.present? } end

def filter_summary return "All users" if active_filters.empty?

parts = []
parts &#x3C;&#x3C; "Role: #{@filters[:role]}" if @filters[:role]
parts &#x3C;&#x3C; "Status: #{@filters[:status]}" if @filters[:status]
parts.join(', ')

end

def export_link h.link_to 'Export CSV', h.users_path(format: :csv, **@filters), class: 'btn btn-secondary' end

private

def h @view_context end end

Repository Pattern

Basic Repository

app/repositories/user_repository.rb

class UserRepository class << self def find(id) User.find(id) end

def find_by_email(email)
  User.find_by(email: email)
end

def active_users
  User.where(active: true).order(created_at: :desc)
end

def search(query)
  User.where('name ILIKE ? OR email ILIKE ?', "%#{query}%", "%#{query}%")
end

def with_recent_orders(days: 30)
  User.joins(:orders)
      .where('orders.created_at > ?', days.days.ago)
      .distinct
end

def create(attributes)
  User.create(attributes)
end

def update(user, attributes)
  user.update(attributes)
end

def destroy(user)
  user.destroy
end

end end

Service using repository

class UserRegistrationService def initialize(repository: UserRepository) @repository = repository end

def call(attributes) user = @repository.create(attributes)

if user.persisted?
  SendWelcomeEmailJob.perform_later(user)
  Result.success(user)
else
  Result.failure(user.errors)
end

end end

Repository with Complex Queries

app/repositories/order_repository.rb

class OrderRepository class << self def pending_orders Order.where(status: 'pending').order(created_at: :asc) end

def overdue_orders(threshold: 3.days)
  Order.where(status: 'pending')
       .where('created_at &#x3C; ?', threshold.ago)
end

def user_orders(user, status: nil)
  scope = user.orders

  scope = scope.where(status: status) if status.present?
  scope.order(created_at: :desc)
end

def revenue_by_month(year: Time.current.year)
  Order.where(status: 'completed')
       .where('EXTRACT(YEAR FROM created_at) = ?', year)
       .group("DATE_TRUNC('month', created_at)")
       .sum(:total)
end

def top_customers(limit: 10)
  User.joins(:orders)
      .where(orders: { status: 'completed' })
      .group('users.id')
      .select('users.*, SUM(orders.total) as total_spent')
      .order('total_spent DESC')
      .limit(limit)
end

end end

PORO (Plain Old Ruby Object) Conventions

Value Objects

app/models/money.rb

class Money include Comparable

attr_reader :amount, :currency

def initialize(amount, currency: 'USD') @amount = BigDecimal(amount.to_s) @currency = currency end

def +(other) validate_currency!(other) Money.new(amount + other.amount, currency: currency) end

def -(other) validate_currency!(other) Money.new(amount - other.amount, currency: currency) end

def *(multiplier) Money.new(amount * multiplier, currency: currency) end

def <=>(other) validate_currency!(other) amount <=> other.amount end

def to_s format('%s%.2f', currency_symbol, amount) end

def ==(other) amount == other.amount && currency == other.currency end

private

def validate_currency!(other) return if currency == other.currency

raise ArgumentError, "Cannot operate on different currencies"

end

def currency_symbol case currency when 'USD' then '$' when 'EUR' then '€' when 'GBP' then '£' else currency end end end

Usage

price = Money.new(19.99) tax = price * 0.08 total = price + tax # => $21.59

Data Transfer Objects (DTOs)

app/models/user_dto.rb

class UserDTO attr_reader :id, :email, :full_name, :role

def initialize(id:, email:, full_name:, role:) @id = id @email = email @full_name = full_name @role = role end

def self.from_model(user) new( id: user.id, email: user.email, full_name: "#{user.first_name} #{user.last_name}", role: user.role ) end

def to_h { id: id, email: email, full_name: full_name, role: role } end end

Or using Ruby 3.2+ Data class

UserDTO = Data.define(:id, :email, :full_name, :role) do def self.from_model(user) new( id: user.id, email: user.email, full_name: "#{user.first_name} #{user.last_name}", role: user.role ) end end

Result Objects

app/models/result.rb

class Result attr_reader :value, :error

def initialize(success:, value: nil, error: nil) @success = success @value = value @error = error end

def self.success(value = nil) new(success: true, value: value) end

def self.failure(error) new(success: false, error: error) end

def success? @success end

def failure? !@success end

def on_success yield value if success? self end

def on_failure yield error if failure? self end end

Service using Result object

class CreateUserService def call(params) user = User.new(params)

if user.save
  Result.success(user)
else
  Result.failure(user.errors)
end

end end

Usage

result = CreateUserService.new.call(user_params)

result .on_success { |user| redirect_to user } .on_failure { |errors| render :new }

Policy Objects

app/policies/post_visibility_policy.rb

class PostVisibilityPolicy def initialize(user, post) @user = user @post = post end

def visible? return true if @post.published? return true if @user&.admin? return true if @post.user_id == @user&.id

false

end

def editable? return true if @user&.admin? return true if @post.user_id == @user&.id

false

end end

Usage in controller

def show @post = Post.find(params[:id]) policy = PostVisibilityPolicy.new(current_user, @post)

unless policy.visible? redirect_to root_path, alert: 'Not authorized' end end

Quick Reference

Before Writing Any Code

Check existing patterns

ls app/services/ ls app/models/ grep -r 'class.Service' app/ --include='.rb' -l | head -10

Check naming conventions

head -30 $(find app/services -name '*.rb' | head -1)

Check dependencies

cat Gemfile | grep -v '^#' | grep -v '^$'

Common File Locations

app/models/ - ActiveRecord models app/controllers/ - Controllers app/services/ - Service objects app/components/ - ViewComponents app/queries/ - Query objects app/forms/ - Form objects app/presenters/ - Presenters app/decorators/ - Decorators app/serializers/ - API serializers app/jobs/ - Background jobs app/mailers/ - Action Mailers app/channels/ - Action Cable channels app/repositories/ - Repository pattern objects app/policies/ - Policy objects (business rules)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rails localization (i18n) - english & arabic

No summary provided by upstream source.

Repository SourceNeeds Review