CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/rails-agent-skills

Curated library of AI agent skills for Ruby on Rails development. Covers code review, architecture, security, testing (RSpec), engines, service objects, DDD patterns, and workflow automation.

98

1.38x
Quality

99%

Does it follow best practices?

Impact

98%

1.38x

Average score across 26 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdruby-service-objects/

name:
ruby-service-objects
description:
Use when creating or refactoring Ruby service classes in Rails. Covers the .call pattern, module namespacing, YARD documentation, standardized responses, orchestrator delegation, transaction wrapping, and error handling conventions.

Ruby Service Objects

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.

Quick Reference

ConventionRule
Entry point.call class method delegating to new.call
Response format{ success: true/false, response: { ... } }
File locationapp/services/module_name/service_name.rb
Pragmafrozen_string_literal: true in every file
DocsYARD on every public method (see yard-documentation)
ValidationValidate method-level inputs at the TOP of call before any loop or business logic — return error hash immediately if invalid
Error handlingEvery rescue block must: (1) log with Rails.logger.error, (2) log backtrace via e.backtrace.join("\n"), (3) return error hash — never re-raise
TransactionsWrap multi-step DB operations

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

Validators raise; the calling service rescues and converts to an error hash.

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

Additional Patterns

Constants for configuration

MISSING_CONFIGURATION_ERROR = 'Missing required configuration'
DEFAULT_TIMEOUT = 30

SQL sanitization

def self.find(tag_number:)
  query = ActiveRecord::Base.sanitize_sql(['SELECT * FROM table WHERE tag_number = ?;', tag_number])
  fetcher.execute_query(query)
end

Checklist for New Service Objects

  • Module namespace matches directory structure
  • Constants defined for error messages and defaults
  • Graceful handling for non-critical failures
  • SQL sanitization for any dynamic queries
  • README.md documenting the module

Pitfalls

ProblemCorrect approach
Response format inconsistencyAlways use { success: bool, response: { ... } }. Error response: { success: false, response: { error: { message: ... } } }. Sub-services handle their own rescue — never let exceptions propagate to callers
Skipping input validationBad input causes cryptic errors deep in the call chain
Transaction wrapping everythingOnly wrap multi-step DB operations that must be atomic
.call method longer than 20 linesExtract to sub-services — orchestrator should coordinate, not implement
Service renders HTTP responsesThat's the controller's job — service returns data only
Service modifies unrelated modelsUnclear boundary — extract a new service with a single responsibility
Duplicated validation across servicesExtract to a shared validator object

Integration

SkillWhen to chain
yard-documentationWhen writing or reviewing inline docs for classes and public methods
ruby-api-client-integrationFor external API integrations (Auth, Client, Fetcher, Builder layers)
strategy-factory-null-calculatorFor variant-based calculators (Factory + Strategy + Null Object)
rspec-service-testingFor testing service objects
rspec-best-practicesFor general RSpec structure
rails-architecture-reviewWhen service extraction is part of an architecture review

ruby-service-objects

README.md

tile.json