CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/rails-agent-skills

Curated library of 39 AI agent skills for Ruby on Rails development. Organized by category: planning, testing, code-quality, ddd, engines, infrastructure, api, patterns, context, orchestration, and workflows. Includes 5 callable workflow skills (rails-tdd-loop, rails-review-flow, rails-setup-flow, rails-quality-flow, rails-engines-flow) for complete development cycles. Covers code review, architecture, security, testing (RSpec), engines, service objects, DDD patterns, and TDD automation.

95

1.20x
Quality

98%

Does it follow best practices?

Impact

95%

1.20x

Average score across 35 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdskills/patterns/ruby-service-objects/

name:
ruby-service-objects
license:
MIT
description:
Use when creating or refactoring Ruby service classes in Rails. Covers the .call pattern, module namespacing, YARD on self.call AND every public method, module README requirement, standardized {success:, response:} response contract, orchestrator delegation, transaction wrapping, and error handling conventions. Trigger words: service object, .call pattern, app/services, service module, service README, response hash, success/response shape, YARD on self.call.

Ruby Service Objects

Conventions

AspectRule
Entry pointdef self.call(...)new(...).call
ValidationValidate inputs at top of call; return error hash if invalid
Error handlingrescue → log + error hash; never re-raise to caller
TransactionsOnly wrap multi-step DB operations that must be atomic
call length≤20 lines; extract sub-services if longer
ScopeReturn data only (no HTTP); single responsibility per service
SQLsanitize_sql for any dynamic queries
Shared logicExtract validators to class-only services (Pattern 3)

Required artifacts (file path, README, YARD, pragma, error log shape) are enumerated once in Output Style.

HARD-GATE: Tests Gate Implementation

EVERY service object MUST have its test written and validated BEFORE implementation.
  1. Write the spec for .call (with contexts for success, error, edge cases)
  2. Run the spec — verify it fails because the service does not exist yet
  3. ONLY THEN write the service implementation
See rspec-best-practices for the full gate cycle.

MANDATORY Response Contract

.call MUST return EXACTLY one of these two shapes — no other top-level keys, never a Boolean, raw model, or ActiveRecord object:

{ success: true,  response: { <domain_key>: <value>, ... } }
{ success: false, response: { error: { message: 'human-readable reason' } } }

When to Use Each Pattern

Signal in the taskPattern
Orchestrates multiple steps, needs instance statePattern 1: .call → new.call
Processes a collection with per-item error handlingPattern 2: Batch processing
Stateless helper, validator, or utility — no instance state neededPattern 3: Class-only (static methods)
Coordinates multiple sub-servicesPattern 4: Orchestrator delegation

Core Patterns

1. The .call Pattern (with delegation, transaction, YARD)

module AnimalTransfers
  class TransferService
    TRANSFER_FAILED = 'Transfer could not be completed'

    # @param params [Hash] :source_shelter_id, :target_shelter_id, :tag_number
    # @return [Hash] { success: Boolean, response: Hash }
    def self.call(params)
      new(params).call
    end

    def initialize(params)
      @source_shelter_id = params[:source_shelter_id]
      @target_shelter_id = params[:target_shelter_id]
      @tag_number = params[:tag_number]
    end

    def call
      source = ShelterValidator.validate_source_shelter!(@source_shelter_id)
      target = ShelterValidator.validate_target_shelter!(@target_shelter_id)
      result = ActiveRecord::Base.transaction do
        source.decrement!(:animal_count)
        target.increment!(:animal_count)
        TransferLog.create!(source:, target:, tag_number: @tag_number)
      end
      { success: true, response: { transfer: result } }
    rescue ActiveRecord::RecordInvalid => e
      Rails.logger.error("Validation Error: #{e.message}")
      Rails.logger.error(e.backtrace.join("\n"))
      { success: false, response: { error: { message: e.message } } }
    rescue StandardError => e
      Rails.logger.error("Processing Error: #{e.message}")
      Rails.logger.error(e.backtrace.join("\n"))
      { success: false, response: { error: { message: TRANSFER_FAILED } } }
    end
  end
end

2. Batch Processing + Per-Item Rescue (Partial Success)

# Batch — each rescue block logs; outer rescue returns { success: false }
def call
  return { success: false, response: { error: { message: 'Items list cannot be empty' } } } if @items.blank?

  results = @items.each_with_object({ successful: [], failed: [] }) do |item, acc|
    validate_item!(item)
    process_item(item)
    acc[:successful] << item[:sku]
  rescue ActiveRecord::RecordNotFound => e
    Rails.logger.error("Item not found: #{e.message}")
    acc[:failed] << { sku: item[:sku], error: e.message }
  rescue StandardError => e
    Rails.logger.error("Unexpected item error: #{e.message}")
    Rails.logger.error(e.backtrace.join("\n"))
    acc[:failed] << { sku: item[:sku], error: e.message }
  end
  { success: true, response: results }
rescue StandardError => e
  Rails.logger.error("Service failed: #{e.message}")
  Rails.logger.error(e.backtrace.join("\n"))
  { success: false, response: { error: { message: PROCESSING_FAILED } } }
end

3. Class-only Services (Static Methods)

When no instance state is needed — use ONLY class methods, no initialize, no instance variables. Validators and stateless helpers should always use this pattern:

class PackageValidator
  MAX_WEIGHT_KG = 30
  MAX_LENGTH_CM = 150

  # @param dimensions [Hash] :weight_kg, :length_cm, :width_cm, :height_cm
  # @return [nil, String] nil if valid, error message otherwise
  def self.validate(dimensions)
    return 'Weight exceeds limit' if dimensions[:weight_kg] > MAX_WEIGHT_KG
    return 'Length exceeds limit' if dimensions[:length_cm] > MAX_LENGTH_CM
    nil
  end

  def self.within_limits?(dimensions)
    validate(dimensions).nil?
  end
end

4. Orchestrator Delegation (≤20-line call)

Sub-services handle their OWN rescue and return { success: false, response: { error: { message: ... } } } on failure. The orchestrator propagates early returns only — no rescue block needed:

# RULE: ≤20 lines in call — if longer, extract another sub-service
def call
  user_result = UserCreationService.call(@params)
  return user_result unless user_result[:success]

  workspace_result = WorkspaceSetupService.call(user_result[:response])
  return workspace_result unless workspace_result[:success]

  BillingService.call(workspace_result[:response])
  NotificationService.call(user_result[:response])
  { success: true, response: { user: user_result[:response] } }
end

Output Style

Every service-object task produces these artifacts:

  1. Service file at app/services/<module_name>/<service_name>.rb — pragma on line 1, class wrapped in a module matching the directory name.
  2. YARD on self.call@param for every argument, @return [Hash], plus @raise for any exception class that can escape (including those rescued internally elsewhere). The self.call wrapper is documented separately from #call, even when it just delegates.
  3. YARD on every other public method — same @param / @return / @raise discipline. See yard-documentation.
  4. Error message constants — user-facing failure strings live in UPPER_SNAKE_CASE constants at the top of the class (e.g. TRANSFER_FAILED = 'Transfer could not be completed'), never inline inside a rescue.
  5. Module README at app/services/<module_name>/README.md — copy the shape from assets/module_readme_template.md. Required even for single-service modules.
  6. Spec file at spec/services/<module_name>/<service_name>_spec.rb written and failing BEFORE the implementation (see HARD-GATE).
  7. English — YARD, README, and error messages in English unless the user requests otherwise.

For class-only services (Pattern 3), the rules apply to the public class methods being documented; if the class returns a non-service shape (e.g. validators returning nil / error string), document that explicitly in YARD and the README.

Integration

SkillWhen to chain
yard-documentationWriting/reviewing inline docs
ruby-api-client-integrationExternal API integrations
strategy-factory-null-calculatorVariant-based calculators
rspec-service-testingTesting service objects
rspec-best-practicesGeneral RSpec structure
rails-architecture-reviewArchitecture review involving service extraction

Assets

skills

README.md

tile.json