スキル一覧に戻る
Kaakati

service-object-patterns

by Kaakati

Enterprise-grade Rails development workflow with multi-agent orchestration, beads task tracking, and quality gates. Generic and portable across any Rails project.

2🍴 0📅 2026年1月24日
GitHubで見るManusで実行

SKILL.md


name: "Service Object Patterns" description: "Complete guide to implementing Service Objects in Ruby on Rails applications. Use when: (1) Creating business logic services, (2) Refactoring fat models/controllers, (3) Organizing service namespaces, (4) Handling service results, (5) Designing service interfaces. Trigger keywords: service objects, business logic, use cases, operations, command pattern, interactors, PORO, application services" version: 1.1.0

Service Object Patterns

Comprehensive guidance for implementing Service Objects in Rails applications.

Service Decision Tree

Where should this logic go?
│
├─ Business logic spanning multiple models?
│   └─ Service Object (app/services/)
│
├─ Operation has multiple steps/side effects?
│   └─ Service Object (with transaction)
│
├─ Need to orchestrate external services?
│   └─ Service Object (with error handling)
│
├─ Complex validation or business rules?
│   └─ Service Object (or Form Object for forms)
│
├─ Simple single-model CRUD?
│   └─ Keep in model/controller
│
└─ Single-line delegation?
    └─ Keep in controller

NEVER Do This

NEVER put business logic in controllers:

# WRONG - Fat controller
def create
  @order = Order.new(order_params)
  @order.calculate_tax
  @order.apply_discount(params[:coupon])
  @order.reserve_inventory
  PaymentGateway.charge(@order.total)
  @order.save
end

# RIGHT - Delegate to service
def create
  @order = OrdersManager::CreateOrder.call(user: current_user, params: order_params)
  redirect_to @order
end

NEVER return raw ActiveRecord errors from services:

# WRONG - Leaking implementation details
def call
  task.save!
rescue ActiveRecord::RecordInvalid => e
  raise e  # Caller must understand AR errors
end

# RIGHT - Wrap in ServiceResult
def call
  task.save!
  ServiceResult.success(task)
rescue ActiveRecord::RecordInvalid => e
  ServiceResult.failure(e.message, errors: task.errors.full_messages)
end

NEVER make service methods public except call:

# WRONG - Exposing internals
class MyService
  def call; end
  def validate_params; end  # Public - shouldn't be
  def process; end          # Public - shouldn't be
end

# RIGHT - Only .call is public
class MyService
  def self.call(...) = new(...).call
  def call; end

  private

  def validate_params; end
  def process; end
end

NEVER skip transactions for multi-step operations:

# WRONG - Partial updates on failure
def call
  task.update!(status: 'completed')
  create_invoice(task)  # If this fails, task is still completed
  notify_customer(task)
end

# RIGHT - All or nothing
def call
  ActiveRecord::Base.transaction do
    task.update!(status: 'completed')
    create_invoice(task)
    notify_customer(task)
  end
end

Directory Structure

app/services/
├── application_service.rb          # Base class
├── tasks_manager/
│   ├── create_task.rb
│   ├── assign_carrier.rb
│   └── complete_task.rb
├── billing_manager/
│   ├── generate_invoice.rb
│   └── process_payment.rb
├── notifications_manager/
│   └── send_sms.rb
└── integrations/
    └── shipping/
        └── create_label.rb

Naming Convention: {Domain}Manager::{Action}


Base Service Class

# app/services/application_service.rb
class ApplicationService
  def self.call(...)
    new(...).call
  end

  private

  attr_reader :params

  def initialize(**params)
    @params = params
  end
end

Basic Service Pattern

module TasksManager
  class CreateTask < ApplicationService
    def initialize(account:, merchant:, params:)
      @account = account
      @merchant = merchant
      @params = params
    end

    def call
      validate_params!

      ActiveRecord::Base.transaction do
        task = build_task
        task.save!
        schedule_notifications(task)
        task
      end
    end

    private

    attr_reader :account, :merchant, :params

    def validate_params!
      raise ArgumentError, "Recipient required" unless params[:recipient_id]
    end

    def build_task
      account.tasks.build(
        merchant: merchant,
        recipient_id: params[:recipient_id],
        status: 'pending'
      )
    end

    def schedule_notifications(task)
      TaskNotificationJob.perform_later(task.id)
    end
  end
end

ServiceResult Pattern

class ServiceResult
  attr_reader :data, :error, :errors

  def initialize(success:, data: nil, error: nil, errors: [])
    @success = success
    @data = data
    @error = error
    @errors = errors
  end

  def success? = @success
  def failure? = !@success

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

  def self.failure(error = nil, errors: [])
    new(success: false, error: error, errors: errors)
  end
end

Usage in Controller

result = TasksManager::AssignCarrier.call(task: @task, carrier: @carrier)

if result.success?
  render json: result.data, status: :ok
else
  render json: { error: result.error, errors: result.errors }, status: :unprocessable_entity
end

Service Interface Guidelines

AspectRule
Entry pointOnly .call is public
DependenciesPass via constructor
ReturnServiceResult or domain object
Side effectsWrap in transactions
ValidationFail fast with clear errors
External APIsHandle timeouts, retries

Error Handling Strategy

Error TypeHandling
Validation errorsReturn ServiceResult.failure
AuthorizationRaise custom AuthorizationError
External APIRetry with backoff, then failure
DatabaseTransaction rollback
UnexpectedLog, track, return generic failure

Service Checklist

Before creating a service:

# Check existing service structure
ls app/services/
ls app/services/*/ 2>/dev/null

# Review existing patterns
head -50 $(find app/services -name '*.rb' | head -1)

# Check naming conventions
grep -r 'class.*Manager' app/services/ --include='*.rb' | head -5

# Verify namespace exists
ls app/services/{namespace}/ 2>/dev/null

Before shipping:

  • Only .call is public
  • All side effects in transaction
  • Returns ServiceResult (not raw errors)
  • Dependencies injected via constructor
  • Input validated at start
  • Specs cover success and failure cases

Quick Reference

PatternUse Case
Basic ServiceSingle business operation
ServiceResultOperations that can fail gracefully
Dry::MonadsComplex multi-step with pattern matching
Service CompositionOrchestrating multiple services
RetriableExternal API calls
Circuit BreakerProtect against cascading failures

References

Detailed patterns and examples in references/:

  • result-patterns.md - ServiceResult, Dry::Monads, fluent API
  • error-handling.md - Custom errors, retry, circuit breaker
  • instrumentation.md - Logging, metrics, error tracking
  • testing.md - RSpec patterns, shared examples, mocking
  • examples.md - Complete service implementations

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です