All skills
sergiodxa avatar

/ruby-on-rails-best-practices

@102fba6

Ruby on Rails architecture and coding patterns from Basecamp. Use when writing, reviewing, or refactoring Rails code to follow proven conventions for models, controllers, jobs, and concerns. Triggers on tasks involving Rails models, concerns, controllers, background jobs, or Turbo/Hotwire.

Use this Skill: https://skilld.dev/gh/sergiodxa/agent-skills/ruby-on-rails-best-practices

This session only. Nothing lands on disk.

rulesthin-jobs.md

≈1.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Keep Jobs Thin

Jobs should be thin wrappers that receive records and call model methods. All business logic belongs in the model layer.

Why

  • Testability: Model methods can be unit tested without job infrastructure
  • Reusability: The same logic can be called sync or async
  • Debuggability: Easier to trace issues when logic isn't buried in jobs
  • Consistency: Models are the single source of truth for domain logic

Bad: Business Logic in Jobs

class ProcessOrderJob < ApplicationJob
  def perform(order)
    return if order.processed?

    order.transaction do
      order.line_items.each do |item|
        item.product.decrement!(:stock, item.quantity)
      end

      order.update!(
        status: :processing,
        processed_at: Time.current
      )

      order.payments.pending.each(&:capture!)
    end

    OrderMailer.confirmation(order).deliver_later
    WebhookService.notify(:order_processed, order)
  end
end

Problems:

  • Can't test processing logic without jobs
  • Can't process synchronously when needed
  • Logic is hidden from model/domain layer

Good: Jobs Delegate to Models

# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
  discard_on ActiveJob::DeserializationError

  def perform(order)
    order.process
  end
end

# app/models/order.rb
class Order < ApplicationRecord
  def process
    return if processed?

    transaction do
      decrement_stock
      mark_as_processing
      capture_payments
    end

    send_confirmation
    notify_webhooks
  end

  def process_later
    ProcessOrderJob.perform_later(self)
  end

  private
    def decrement_stock
      line_items.each do |item|
        item.product.decrement!(:stock, item.quantity)
      end
    end

    def mark_as_processing
      update!(status: :processing, processed_at: Time.current)
    end

    def capture_payments
      payments.pending.each(&:capture!)
    end

    def send_confirmation
      OrderMailer.confirmation(self).deliver_later
    end

    def notify_webhooks
      WebhookService.notify(:order_processed, self)
    end
end

Job Responsibilities

Jobs should ONLY:

  1. Receive arguments (records, simple values)
  2. Call a single model method
  3. Handle job-specific concerns (retries, discards, queues)
class Card::ActivitySpike::DetectionJob < ApplicationJob
  discard_on ActiveJob::DeserializationError

  def perform(card)
    card.detect_activity_spikes  # Single method call
  end
end

class Notification::Bundle::DeliverJob < ApplicationJob
  include SmtpDeliveryErrorHandling  # Job concern for retries
  queue_as :backend
  discard_on ActiveJob::DeserializationError

  def perform(bundle)
    bundle.deliver  # Single method call
  end
end

When Jobs Can Have More Logic

Batch Operations

Jobs that process collections may have iteration logic:

class DeleteUnusedTagsJob < ApplicationJob
  def perform
    Tag.unused.find_each do |tag|
      tag.destroy!
    end
  end
end

But even here, consider a class method:

# Better: model class method
class Tag < ApplicationRecord
  def self.delete_unused
    unused.find_each(&:destroy!)
  end
end

class DeleteUnusedTagsJob < ApplicationJob
  def perform
    Tag.delete_unused
  end
end

Keyword Arguments

Jobs can accept keyword arguments alongside records:

class Mention::CreateJob < ApplicationJob
  discard_on ActiveJob::DeserializationError

  def perform(record, mentioner:)
    record.create_mentions(mentioner: mentioner)
  end
end

Job Naming Convention

Jobs should be namespaced to mirror the model they operate on:

Model/Concern Job
Card::Accessible Card::CleanInaccessibleDataJob
Card::Stallable Card::ActivitySpike::DetectionJob
Storage::Totaled Storage::MaterializeJob
Notification::Bundle Notification::Bundle::DeliverJob
Webhook::Delivery Webhook::DeliveryJob

Rules

  1. Jobs call one method on the received record
  2. All business logic lives in models
  3. Jobs handle only job-specific concerns (queues, retries, error handling)
  4. Namespace jobs to mirror model structure
  5. Always include discard_on ActiveJob::DeserializationError

Source: SKILL.md on GitHub

1 warning17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides architectural guidelines and coding patterns for Ruby on Rails development based on Basecamp's practices. No security issues were detected.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    17/17 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 102fba6. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 8 months ago

README badge

README badge for sergiodxa/agent-skills/ruby-on-rails-best-practices