Claude Code Plugins

Community-maintained marketplace

Feedback

Service Object Patterns

@Kaakati/rails-enterprise-dev
1
0

Complete guide to implementing Service Objects in Ruby on Rails applications. Use this skill when creating business logic services, organizing service namespaces, handling service results, and designing service interfaces for complex operations.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name Service Object Patterns
description Complete guide to implementing Service Objects in Ruby on Rails applications. Use this skill when creating business logic services, organizing service namespaces, handling service results, and designing service interfaces for complex operations.

Service Object Patterns Skill

This skill provides comprehensive guidance for implementing Service Objects in Rails applications following consistent patterns and conventions.

When to Use This Skill

  • Creating new service objects for business logic
  • Refactoring fat models or controllers
  • Designing service interfaces
  • Implementing result objects for service responses
  • Organizing services into namespaces

When to Use Service Objects

Use Service Objects When:

  • Business logic spans multiple models
  • Operation has multiple steps/side effects
  • Logic doesn't naturally belong to one model
  • Need to orchestrate external services
  • Complex validation or business rules
  • Operation needs transaction management

Don't Use Service Objects When:

  • Simple CRUD operations
  • Logic clearly belongs to one model
  • Single-line delegation
  • No side effects beyond model updates

Directory Structure

app/services/
├── application_service.rb          # Base class
├── tasks_manager/
│   ├── create_task.rb
│   ├── assign_carrier.rb
│   ├── complete_task.rb
│   └── bundling/
│       ├── bundle_tasks.rb
│       └── optimize_routes.rb
├── billing_manager/
│   ├── generate_invoice.rb
│   ├── process_payment.rb
│   └── calculate_fees.rb
├── notifications_manager/
│   ├── send_sms.rb
│   └── send_push_notification.rb
└── integrations/
    ├── salla/
    │   └── sync_orders.rb
    └── shipping/
        └── create_label.rb

Naming Convention

# Pattern: {Domain}Manager::{Action} or {Domain}Manager::{SubDomain}::{Action}

# Examples:
TasksManager::CreateTask
TasksManager::Bundling::BundleTasks
BillingManager::GenerateInvoice
IntegrationsManager::Salla::SyncOrders

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

# app/services/tasks_manager/create_task.rb
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
        assign_zone(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]
      raise ArgumentError, "Address required" unless params[:address]
    end

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

    def assign_zone(task)
      zone = ZoneFinder.new(account, params[:address]).find
      task.zone = zone
    end

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

# Usage:
task = TasksManager::CreateTask.call(
  account: current_account,
  merchant: merchant,
  params: task_params
)

Result Object Pattern

For services that need structured success/failure responses:

# app/services/service_result.rb
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
  end

  def failure?
    !@success
  end

  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
# app/services/tasks_manager/assign_carrier.rb
module TasksManager
  class AssignCarrier < ApplicationService
    def initialize(task:, carrier:)
      @task = task
      @carrier = carrier
    end

    def call
      return ServiceResult.failure("Task already assigned") if task.carrier.present?
      return ServiceResult.failure("Carrier not available") unless carrier_available?
      return ServiceResult.failure("Carrier not in zone") unless carrier_in_zone?

      ActiveRecord::Base.transaction do
        task.update!(carrier: carrier, assigned_at: Time.current)
        notify_carrier
        notify_recipient
      end

      ServiceResult.success(task.reload)
    rescue ActiveRecord::RecordInvalid => e
      ServiceResult.failure(e.message, errors: task.errors.full_messages)
    end

    private

    attr_reader :task, :carrier

    def carrier_available?
      carrier.active? && carrier.available?
    end

    def carrier_in_zone?
      return true unless task.zone
      carrier.zones.include?(task.zone)
    end

    def notify_carrier
      CarrierNotificationJob.perform_later(carrier.id, task.id)
    end

    def notify_recipient
      RecipientNotificationJob.perform_later(task.id, :carrier_assigned)
    end
  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

Dry-Monads Pattern (Alternative)

If using the dry-monads gem:

# Gemfile
gem 'dry-monads'

# app/services/tasks_manager/complete_task.rb
module TasksManager
  class CompleteTask
    include Dry::Monads[:result, :do]

    def initialize(task:, otp:, photos: [])
      @task = task
      @otp = otp
      @photos = photos
    end

    def call
      yield validate_otp
      yield validate_photos
      yield complete_task
      yield process_payment
      yield notify_parties

      Success(task.reload)
    end

    private

    attr_reader :task, :otp, :photos

    def validate_otp
      return Failure(:invalid_otp) unless task.otp == otp
      Success()
    end

    def validate_photos
      return Failure(:photos_required) if task.requires_photos? && photos.empty?
      Success()
    end

    def complete_task
      task.update!(
        status: 'completed',
        completed_at: Time.current
      )
      Success()
    rescue ActiveRecord::RecordInvalid => e
      Failure(e.message)
    end

    def process_payment
      # Payment processing logic
      Success()
    end

    def notify_parties
      TaskCompletionNotificationJob.perform_later(task.id)
      Success()
    end
  end
end

# Usage:
result = TasksManager::CompleteTask.new(task: @task, otp: params[:otp]).call

case result
in Success(task)
  render json: task
in Failure(:invalid_otp)
  render json: { error: "Invalid OTP" }, status: :unprocessable_entity
in Failure(error)
  render json: { error: error }, status: :unprocessable_entity
end

Service Composition

For complex operations that coordinate multiple services:

# app/services/tasks_manager/process_delivery.rb
module TasksManager
  class ProcessDelivery < ApplicationService
    def initialize(task:, carrier:, params:)
      @task = task
      @carrier = carrier
      @params = params
    end

    def call
      ActiveRecord::Base.transaction do
        validate_delivery!
        complete_task!
        process_cod! if task.cod?
        generate_invoice!
        notify_all_parties!
      end

      ServiceResult.success(task.reload)
    rescue StandardError => e
      ServiceResult.failure(e.message)
    end

    private

    attr_reader :task, :carrier, :params

    def validate_delivery!
      result = DeliveryValidator.call(task: task, params: params)
      raise result.error unless result.success?
    end

    def complete_task!
      result = CompleteTask.call(
        task: task,
        otp: params[:otp],
        photos: params[:photos]
      )
      raise result.error unless result.success?
    end

    def process_cod!
      result = BillingManager::ProcessCod.call(
        task: task,
        carrier: carrier,
        amount: task.cod_amount
      )
      raise result.error unless result.success?
    end

    def generate_invoice!
      BillingManager::GenerateInvoice.call(task: task)
    end

    def notify_all_parties!
      NotificationsManager::DeliveryComplete.call(task: task)
    end
  end
end

Service with External API

# app/services/integrations/shipping/create_label.rb
module Integrations
  module Shipping
    class CreateLabel < ApplicationService
      TIMEOUT = 30.seconds

      def initialize(task:, shipping_company:)
        @task = task
        @shipping_company = shipping_company
      end

      def call
        response = make_api_request
        
        if response.success?
          label = create_label_record(response.body)
          ServiceResult.success(label)
        else
          handle_error(response)
        end
      rescue Faraday::TimeoutError
        ServiceResult.failure("Shipping API timeout")
      rescue Faraday::ConnectionFailed
        ServiceResult.failure("Unable to connect to shipping API")
      end

      private

      attr_reader :task, :shipping_company

      def make_api_request
        client.post('/labels', label_payload)
      end

      def client
        @client ||= Faraday.new(url: shipping_company.api_url) do |f|
          f.request :json
          f.response :json
          f.options.timeout = TIMEOUT
          f.headers['Authorization'] = "Bearer #{shipping_company.api_key}"
        end
      end

      def label_payload
        {
          sender: sender_details,
          recipient: recipient_details,
          package: package_details
        }
      end

      def create_label_record(response_body)
        task.create_shipping_label!(
          tracking_number: response_body['tracking_number'],
          label_url: response_body['label_url'],
          shipping_company: shipping_company
        )
      end

      def handle_error(response)
        error_message = response.body['error'] || "API Error: #{response.status}"
        Rails.logger.error("Shipping API Error: #{error_message}")
        ServiceResult.failure(error_message)
      end
    end
  end
end

Service with Background Jobs

# app/services/tasks_manager/bulk_import.rb
module TasksManager
  class BulkImport < ApplicationService
    def initialize(account:, file:, user:)
      @account = account
      @file = file
      @user = user
    end

    def call
      import = create_import_record
      schedule_processing(import)
      ServiceResult.success(import)
    end

    private

    attr_reader :account, :file, :user

    def create_import_record
      account.task_imports.create!(
        file: file,
        user: user,
        status: 'pending',
        total_rows: count_rows
      )
    end

    def schedule_processing(import)
      BulkImportJob.perform_later(import.id)
    end

    def count_rows
      # Count rows in uploaded file
      CSV.read(file.path).count - 1  # Minus header
    end
  end
end

Testing Services

# spec/services/tasks_manager/create_task_spec.rb
require 'rails_helper'

RSpec.describe TasksManager::CreateTask do
  let(:account) { create(:account) }
  let(:merchant) { create(:merchant, account: account) }
  let(:recipient) { create(:recipient, account: account) }
  
  let(:valid_params) do
    {
      recipient_id: recipient.id,
      description: "Test delivery",
      amount: 100,
      address: "123 Test St"
    }
  end

  describe '.call' do
    context 'with valid params' do
      it 'creates a task' do
        expect {
          described_class.call(
            account: account,
            merchant: merchant,
            params: valid_params
          )
        }.to change(Task, :count).by(1)
      end

      it 'assigns the zone' do
        task = described_class.call(
          account: account,
          merchant: merchant,
          params: valid_params
        )
        
        expect(task.zone).to be_present
      end

      it 'schedules notification' do
        expect {
          described_class.call(
            account: account,
            merchant: merchant,
            params: valid_params
          )
        }.to have_enqueued_job(TaskNotificationJob)
      end
    end

    context 'with invalid params' do
      it 'raises error without recipient' do
        invalid_params = valid_params.except(:recipient_id)
        
        expect {
          described_class.call(
            account: account,
            merchant: merchant,
            params: invalid_params
          )
        }.to raise_error(ArgumentError, "Recipient required")
      end
    end
  end
end

Service Interface Guidelines

Method Visibility

class MyService
  # PUBLIC: Only .call is public (entry point)
  def self.call(...)
    new(...).call
  end

  def call
    # Main logic
  end

  private

  # PRIVATE: All other methods are private
  attr_reader :params

  def validate!
    # validation
  end

  def process
    # processing
  end
end

Input Validation

def initialize(user:, params:)
  @user = user
  @params = params
  validate_input!
end

private

def validate_input!
  raise ArgumentError, "User required" unless @user
  raise ArgumentError, "Params required" unless @params
end

Transaction Management

def call
  ActiveRecord::Base.transaction do
    step_one
    step_two
    step_three
  end
rescue ActiveRecord::RecordInvalid => e
  # Handle validation errors
  ServiceResult.failure(e.message)
rescue StandardError => e
  # Handle other errors
  Rails.logger.error("Service error: #{e.message}")
  ServiceResult.failure("An error occurred")
end

Comprehensive Error Handling

Custom Error Classes

# app/services/errors.rb
module Services
  module Errors
    class ServiceError < StandardError
      attr_reader :context

      def initialize(message = nil, context: {})
        @context = context
        super(message)
      end
    end

    class ValidationError < ServiceError; end
    class AuthorizationError < ServiceError; end
    class ExternalServiceError < ServiceError; end
    class TimeoutError < ServiceError; end
    class RateLimitError < ServiceError; end
    class ResourceNotFoundError < ServiceError; end
  end
end

# Usage in service
module TasksManager
  class CreateTask < ApplicationService
    def call
      validate_authorization!
      validate_params!

      task = build_and_save_task
      ServiceResult.success(task)
    rescue Services::Errors::ValidationError => e
      ServiceResult.failure(e.message, errors: e.context[:errors])
    rescue Services::Errors::AuthorizationError => e
      ServiceResult.failure("Not authorized", context: e.context)
    end

    private

    def validate_authorization!
      unless @user.can?(:create_task, @account)
        raise Services::Errors::AuthorizationError.new(
          "User not authorized to create tasks",
          context: { user_id: @user.id, account_id: @account.id }
        )
      end
    end

    def validate_params!
      errors = []
      errors << "Recipient required" unless @params[:recipient_id]
      errors << "Address required" unless @params[:address]

      if errors.any?
        raise Services::Errors::ValidationError.new(
          "Validation failed",
          context: { errors: errors }
        )
      end
    end
  end
end

Error Handler Concern

# app/services/concerns/error_handling.rb
module Services
  module Concerns
    module ErrorHandling
      extend ActiveSupport::Concern

      included do
        rescue_from StandardError, with: :handle_standard_error
        rescue_from ActiveRecord::RecordInvalid, with: :handle_record_invalid
        rescue_from Services::Errors::ServiceError, with: :handle_service_error
      end

      private

      def handle_standard_error(exception)
        log_error(exception)
        track_error(exception)
        ServiceResult.failure("An unexpected error occurred")
      end

      def handle_record_invalid(exception)
        log_error(exception)
        ServiceResult.failure(
          "Validation failed",
          errors: exception.record.errors.full_messages
        )
      end

      def handle_service_error(exception)
        log_error(exception, context: exception.context)
        ServiceResult.failure(exception.message, context: exception.context)
      end

      def log_error(exception, context: {})
        Rails.logger.error({
          error_class: exception.class.name,
          error_message: exception.message,
          backtrace: exception.backtrace.first(5),
          context: context,
          service: self.class.name
        }.to_json)
      end

      def track_error(exception)
        # Integrate with error tracking service (Sentry, Rollbar, etc.)
        if defined?(Sentry)
          Sentry.capture_exception(exception, extra: {
            service: self.class.name,
            params: sanitized_params
          })
        end
      end

      def sanitized_params
        # Remove sensitive data before logging
        @params.except(:password, :token, :api_key)
      end
    end
  end
end

# Usage
module TasksManager
  class CreateTask < ApplicationService
    include Services::Concerns::ErrorHandling

    def call
      # Implementation
    end
  end
end

Retry Mechanisms

# app/services/concerns/retriable.rb
module Services
  module Concerns
    module Retriable
      extend ActiveSupport::Concern

      RETRYABLE_ERRORS = [
        Faraday::TimeoutError,
        Faraday::ConnectionFailed,
        Services::Errors::TimeoutError,
        ActiveRecord::Deadlocked
      ].freeze

      def with_retry(max_attempts: 3, backoff: 2, &block)
        attempt = 1

        begin
          yield
        rescue *RETRYABLE_ERRORS => e
          if attempt < max_attempts
            sleep_duration = backoff**attempt
            Rails.logger.warn(
              "Retrying after error (attempt #{attempt}/#{max_attempts}): #{e.message}. " \
              "Sleeping #{sleep_duration}s"
            )
            sleep(sleep_duration)
            attempt += 1
            retry
          else
            Rails.logger.error("Max retry attempts (#{max_attempts}) exceeded: #{e.message}")
            raise
          end
        end
      end
    end
  end
end

# Usage
module Integrations
  module Shipping
    class CreateLabel < ApplicationService
      include Services::Concerns::Retriable

      def call
        with_retry(max_attempts: 3, backoff: 2) do
          response = make_api_request
          process_response(response)
        end
      rescue Faraday::TimeoutError => e
        ServiceResult.failure("Shipping API timeout after retries")
      end
    end
  end
end

Circuit Breaker Pattern

# app/services/concerns/circuit_breaker.rb
module Services
  module Concerns
    module CircuitBreaker
      extend ActiveSupport::Concern

      class_methods do
        def circuit_breaker(service_name, failure_threshold: 5, timeout: 60)
          @circuit_state ||= {}
          @circuit_state[service_name] ||= {
            failures: 0,
            last_failure_time: nil,
            state: :closed  # :closed, :open, :half_open
          }
        end

        def circuit_open?(service_name)
          circuit = @circuit_state[service_name]
          return false unless circuit

          if circuit[:state] == :open
            # Check if timeout period has passed
            if Time.current - circuit[:last_failure_time] > circuit[:timeout]
              circuit[:state] = :half_open
              false
            else
              true
            end
          else
            false
          end
        end

        def record_success(service_name)
          circuit = @circuit_state[service_name]
          return unless circuit

          circuit[:failures] = 0
          circuit[:state] = :closed
        end

        def record_failure(service_name)
          circuit = @circuit_state[service_name]
          return unless circuit

          circuit[:failures] += 1
          circuit[:last_failure_time] = Time.current

          if circuit[:failures] >= circuit[:failure_threshold]
            circuit[:state] = :open
            Rails.logger.warn("Circuit breaker opened for #{service_name}")
          end
        end
      end

      def with_circuit_breaker(service_name, &block)
        if self.class.circuit_open?(service_name)
          raise Services::Errors::ExternalServiceError.new(
            "Circuit breaker open for #{service_name}",
            context: { service: service_name }
          )
        end

        result = yield
        self.class.record_success(service_name)
        result
      rescue StandardError => e
        self.class.record_failure(service_name)
        raise
      end
    end
  end
end

# Usage
module Integrations
  module Shipping
    class CreateLabel < ApplicationService
      include Services::Concerns::CircuitBreaker

      circuit_breaker :shipping_api, failure_threshold: 5, timeout: 60

      def call
        with_circuit_breaker(:shipping_api) do
          response = make_api_request
          process_response(response)
        end
      rescue Services::Errors::ExternalServiceError => e
        ServiceResult.failure(e.message)
      end
    end
  end
end

Instrumentation & Monitoring

Performance Instrumentation

# app/services/concerns/instrumentation.rb
module Services
  module Concerns
    module Instrumentation
      extend ActiveSupport::Concern

      included do
        around_action :instrument_service_call, only: :call
      end

      private

      def instrument_service_call
        start_time = Time.current
        service_name = self.class.name.underscore.tr('/', '.')

        ActiveSupport::Notifications.instrument(
          "service.call",
          service: service_name,
          params: sanitized_params
        ) do
          result = yield

          duration = (Time.current - start_time) * 1000  # Convert to ms

          log_performance(service_name, duration, result)
          track_metrics(service_name, duration, result)

          result
        end
      end

      def log_performance(service_name, duration, result)
        Rails.logger.info({
          service: service_name,
          duration_ms: duration.round(2),
          success: result.success?,
          timestamp: Time.current.iso8601
        }.to_json)
      end

      def track_metrics(service_name, duration, result)
        # StatsD integration
        if defined?(StatsD)
          StatsD.increment("service.calls", tags: [
            "service:#{service_name}",
            "status:#{result.success? ? 'success' : 'failure'}"
          ])

          StatsD.timing("service.duration", duration, tags: [
            "service:#{service_name}"
          ])
        end

        # Prometheus integration
        if defined?(PrometheusExporter)
          PrometheusExporter::Client.default.send_json(
            type: "service_call",
            service: service_name,
            duration: duration,
            success: result.success?
          )
        end
      end
    end
  end
end

Structured Logging

# app/services/concerns/loggable.rb
module Services
  module Concerns
    module Loggable
      extend ActiveSupport::Concern

      private

      def log_info(message, context = {})
        log(:info, message, context)
      end

      def log_warn(message, context = {})
        log(:warn, message, context)
      end

      def log_error(message, context = {})
        log(:error, message, context)
      end

      def log_debug(message, context = {})
        log(:debug, message, context)
      end

      def log(level, message, context = {})
        Rails.logger.public_send(level, {
          service: self.class.name,
          message: message,
          timestamp: Time.current.iso8601,
          **context
        }.to_json)
      end

      def log_service_start(context = {})
        log_info("Service started", {
          params: sanitized_params,
          **context
        })
      end

      def log_service_complete(result, context = {})
        log_info("Service completed", {
          success: result.success?,
          duration_ms: context[:duration_ms],
          **context
        })
      end

      def log_external_api_call(api_name, endpoint, context = {})
        log_info("External API call", {
          api: api_name,
          endpoint: endpoint,
          **context
        })
      end
    end
  end
end

# Usage
module Integrations
  module Shipping
    class CreateLabel < ApplicationService
      include Services::Concerns::Loggable

      def call
        log_service_start(task_id: @task.id)
        start_time = Time.current

        log_external_api_call("Shipping API", "/labels", {
          task_id: @task.id,
          shipping_company: @shipping_company.name
        })

        response = make_api_request
        result = process_response(response)

        duration = (Time.current - start_time) * 1000
        log_service_complete(result, duration_ms: duration)

        result
      end
    end
  end
end

Metrics Collection

# app/services/concerns/metrics.rb
module Services
  module Concerns
    module Metrics
      extend ActiveSupport::Concern

      def track_counter(metric_name, value = 1, tags: {})
        if defined?(StatsD)
          StatsD.increment(
            "service.#{metric_name}",
            value,
            tags: format_tags(tags)
          )
        end
      end

      def track_gauge(metric_name, value, tags: {})
        if defined?(StatsD)
          StatsD.gauge(
            "service.#{metric_name}",
            value,
            tags: format_tags(tags)
          )
        end
      end

      def track_timing(metric_name, duration_ms, tags: {})
        if defined?(StatsD)
          StatsD.timing(
            "service.#{metric_name}",
            duration_ms,
            tags: format_tags(tags)
          )
        end
      end

      def track_histogram(metric_name, value, tags: {})
        if defined?(StatsD)
          StatsD.histogram(
            "service.#{metric_name}",
            value,
            tags: format_tags(tags)
          )
        end
      end

      private

      def format_tags(tags)
        default_tags.merge(tags).map { |k, v| "#{k}:#{v}" }
      end

      def default_tags
        {
          service: self.class.name.underscore.tr('/', '.'),
          environment: Rails.env
        }
      end
    end
  end
end

# Usage
module TasksManager
  class CreateTask < ApplicationService
    include Services::Concerns::Metrics

    def call
      start_time = Time.current

      task = build_and_save_task
      track_counter("task.created", tags: { merchant_id: @merchant.id })

      duration = (Time.current - start_time) * 1000
      track_timing("task.creation_time", duration)

      ServiceResult.success(task)
    end
  end
end

ActiveSupport Notifications

# app/services/tasks_manager/create_task.rb
module TasksManager
  class CreateTask < ApplicationService
    def call
      ActiveSupport::Notifications.instrument(
        "task.create",
        account_id: @account.id,
        merchant_id: @merchant.id
      ) do |payload|
        task = build_and_save_task

        payload[:task_id] = task.id
        payload[:zone_id] = task.zone_id

        ServiceResult.success(task)
      end
    end
  end
end

# config/initializers/service_notifications.rb
ActiveSupport::Notifications.subscribe("task.create") do |name, start, finish, id, payload|
  duration = (finish - start) * 1000

  Rails.logger.info({
    event: name,
    duration_ms: duration.round(2),
    account_id: payload[:account_id],
    task_id: payload[:task_id]
  }.to_json)

  # Send metrics
  StatsD.increment("task.created", tags: [
    "account:#{payload[:account_id]}",
    "merchant:#{payload[:merchant_id]}"
  ]) if defined?(StatsD)

  StatsD.timing("task.creation_duration", duration, tags: [
    "account:#{payload[:account_id]}"
  ]) if defined?(StatsD)
end

Error Tracking Integration

# app/services/concerns/error_tracking.rb
module Services
  module Concerns
    module ErrorTracking
      extend ActiveSupport::Concern

      private

      def capture_exception(exception, context: {})
        # Sentry integration
        if defined?(Sentry)
          Sentry.capture_exception(exception, extra: {
            service: self.class.name,
            context: context,
            params: sanitized_params
          })
        end

        # Rollbar integration
        if defined?(Rollbar)
          Rollbar.error(exception, {
            service: self.class.name,
            context: context,
            params: sanitized_params
          })
        end

        # Airbrake integration
        if defined?(Airbrake)
          Airbrake.notify(exception, {
            service: self.class.name,
            context: context,
            params: sanitized_params
          })
        end
      end

      def capture_message(message, level: :info, context: {})
        if defined?(Sentry)
          Sentry.capture_message(message, level: level, extra: {
            service: self.class.name,
            context: context
          })
        end
      end
    end
  end
end

# Usage
module TasksManager
  class CreateTask < ApplicationService
    include Services::Concerns::ErrorTracking

    def call
      task = build_and_save_task
      ServiceResult.success(task)
    rescue StandardError => e
      capture_exception(e, context: {
        account_id: @account.id,
        merchant_id: @merchant.id,
        params: @params
      })
      ServiceResult.failure("Failed to create task")
    end
  end
end

Comprehensive Service Template

# app/services/tasks_manager/create_task.rb
module TasksManager
  class CreateTask < ApplicationService
    include Services::Concerns::ErrorHandling
    include Services::Concerns::Retriable
    include Services::Concerns::Instrumentation
    include Services::Concerns::Loggable
    include Services::Concerns::Metrics
    include Services::Concerns::ErrorTracking

    def initialize(account:, merchant:, params:, user:)
      @account = account
      @merchant = merchant
      @params = params
      @user = user
    end

    def call
      log_service_start(account_id: @account.id, merchant_id: @merchant.id)
      start_time = Time.current

      validate_authorization!
      validate_params!

      task = with_retry(max_attempts: 3) do
        build_and_save_task
      end

      track_counter("task.created", tags: { merchant_id: @merchant.id })
      duration = (Time.current - start_time) * 1000
      track_timing("task.creation_time", duration)

      log_service_complete(ServiceResult.success(task), duration_ms: duration)

      ServiceResult.success(task)
    rescue Services::Errors::ServiceError => e
      capture_exception(e, context: { account_id: @account.id })
      ServiceResult.failure(e.message, context: e.context)
    rescue StandardError => e
      log_error("Unexpected error", error: e.message, backtrace: e.backtrace.first(5))
      capture_exception(e, context: { account_id: @account.id })
      ServiceResult.failure("An unexpected error occurred")
    end

    private

    attr_reader :account, :merchant, :params, :user

    def validate_authorization!
      unless user.can?(:create_task, account)
        raise Services::Errors::AuthorizationError.new(
          "User not authorized",
          context: { user_id: user.id, account_id: account.id }
        )
      end
    end

    def validate_params!
      errors = []
      errors << "Recipient required" unless params[:recipient_id]
      errors << "Address required" unless params[:address]

      if errors.any?
        raise Services::Errors::ValidationError.new(
          "Validation failed",
          context: { errors: errors }
        )
      end
    end

    def build_and_save_task
      ActiveRecord::Base.transaction do
        task = account.tasks.build(
          merchant: merchant,
          recipient_id: params[:recipient_id],
          description: params[:description],
          amount: params[:amount],
          status: 'pending'
        )

        assign_zone(task)
        task.save!

        log_info("Task created", task_id: task.id)
        schedule_notifications(task)

        task
      end
    end

    def assign_zone(task)
      zone = ZoneFinder.new(account, params[:address]).find
      task.zone = zone
      log_debug("Zone assigned", zone_id: zone&.id)
    end

    def schedule_notifications(task)
      TaskNotificationJob.perform_later(task.id)
      log_debug("Notifications scheduled", task_id: task.id)
    end
  end
end

Pre-Creation Checklist

Before creating a service:

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

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

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

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

# 5. Check for existing concerns
ls app/services/concerns/ 2>/dev/null

# 6. Review error handling patterns
grep -r 'ServiceError' app/services/ --include='*.rb' | head -5