CrackedRuby logo

CrackedRuby

Service Objects

A comprehensive guide to implementing and using Service Objects as a design pattern for encapsulating business logic in Ruby applications.

Patterns and Best Practices Code Organization
11.3.3

Overview

Service Objects represent a design pattern that encapsulates specific business operations into single-purpose classes. Ruby applications use Service Objects to extract complex logic from models and controllers, creating focused classes that handle one specific task or business process.

The Service Object pattern addresses code organization issues by moving business logic into dedicated classes. Each Service Object typically implements a single public interface method, commonly named call, perform, or execute. This approach separates business logic from framework concerns and creates testable, reusable components.

Ruby Service Objects follow object-oriented principles by encapsulating data and behavior. They receive input parameters through initialization or method arguments, perform operations, and return results. The pattern works particularly well for operations that span multiple models, integrate with external services, or implement complex business rules.

class CreateUserAccount
  def initialize(email:, password:, subscription_plan:)
    @email = email
    @password = password
    @subscription_plan = subscription_plan
  end

  def call
    user = User.create!(email: @email, password: @password)
    subscription = create_subscription(user)
    send_welcome_email(user)
    
    { user: user, subscription: subscription }
  end

  private

  def create_subscription(user)
    Subscription.create!(
      user: user,
      plan: @subscription_plan,
      status: 'active'
    )
  end

  def send_welcome_email(user)
    UserMailer.welcome(@email).deliver_later
  end
end

Service Objects provide several architectural benefits. They isolate business logic from framework dependencies, making code more testable and maintainable. They create explicit boundaries around operations, improving code clarity and reducing coupling between components. The pattern also facilitates code reuse across different parts of an application.

# Usage example
result = CreateUserAccount.new(
  email: 'user@example.com',
  password: 'secure_password',
  subscription_plan: 'premium'
).call

user = result[:user]
subscription = result[:subscription]

Common Service Object implementations include data processing operations, external API integrations, complex validations, report generation, and multi-step business workflows. The pattern scales from simple operations to complex orchestrations involving multiple systems and business rules.

Basic Usage

Service Objects implement business operations through focused classes with clear interfaces. The most common implementation pattern uses an initializer to accept dependencies and parameters, followed by a call method that executes the operation.

class ProcessPayment
  def initialize(order:, payment_method:)
    @order = order
    @payment_method = payment_method
  end

  def call
    validate_payment_method
    charge_payment
    update_order_status
    
    @order.reload
  end

  private

  def validate_payment_method
    raise ArgumentError, 'Invalid payment method' unless @payment_method.valid?
  end

  def charge_payment
    PaymentGateway.charge(
      amount: @order.total,
      source: @payment_method.token
    )
  end

  def update_order_status
    @order.update!(status: 'paid', paid_at: Time.current)
  end
end

The Service Object constructor receives all necessary dependencies and data. This approach makes dependencies explicit and supports dependency injection for testing. Parameters can include domain objects, primitive values, or configuration objects.

Service Objects handle input validation within their operations. Validation occurs early in the call method execution, preventing invalid operations from proceeding. Some implementations separate validation into dedicated private methods for clarity.

class GenerateReport
  def initialize(user:, date_range:, format: :pdf)
    @user = user
    @date_range = date_range
    @format = format
  end

  def call
    validate_inputs
    
    data = collect_report_data
    formatted_report = format_report(data)
    
    store_report(formatted_report)
  end

  private

  def validate_inputs
    raise ArgumentError, 'Date range required' if @date_range.nil?
    raise ArgumentError, 'Invalid format' unless [:pdf, :csv, :json].include?(@format)
  end

  def collect_report_data
    @user.transactions.where(created_at: @date_range)
  end

  def format_report(data)
    case @format
    when :pdf
      PdfGenerator.new(data).generate
    when :csv
      CsvGenerator.new(data).generate
    when :json
      data.to_json
    end
  end

  def store_report(report)
    ReportStorage.save(@user.id, report, @format)
  end
end

Return values from Service Objects vary based on requirements. Simple operations might return boolean success indicators, while complex operations return structured data or result objects. Consistent return patterns within an application improve predictability.

Class method shortcuts provide alternative invocation patterns. Many Service Objects implement self.call methods that instantiate and invoke the service in one operation. This pattern reduces boilerplate for simple use cases.

class SendNotification
  def self.call(user:, message:, type: :email)
    new(user: user, message: message, type: type).call
  end

  def initialize(user:, message:, type:)
    @user = user
    @message = message
    @type = type
  end

  def call
    case @type
    when :email
      send_email
    when :sms
      send_sms
    when :push
      send_push_notification
    end
  end

  private

  def send_email
    NotificationMailer.send_message(@user.email, @message).deliver_now
  end

  def send_sms
    SmsService.send(@user.phone, @message)
  end

  def send_push_notification
    PushNotificationService.send(@user.device_tokens, @message)
  end
end

# Usage with class method
SendNotification.call(
  user: current_user,
  message: "Your order has shipped",
  type: :email
)

Service Objects integrate naturally with Rails controllers and background jobs. Controllers delegate business logic to Service Objects, keeping controllers thin and focused on HTTP concerns. Background jobs use Service Objects to perform asynchronous operations with the same business logic used in synchronous contexts.

Advanced Usage

Complex Service Objects implement sophisticated patterns including composition, result objects, and conditional execution flows. These patterns handle intricate business requirements while maintaining code organization and testability.

Result objects encapsulate operation outcomes with success states, data, and error information. This pattern provides structured communication between Service Objects and calling code, supporting both success and failure scenarios with detailed context.

class Result
  attr_reader :data, :errors

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

  def success?
    @success
  end

  def failure?
    !@success
  end
end

class ImportCustomers
  def initialize(csv_file:, batch_size: 100)
    @csv_file = csv_file
    @batch_size = batch_size
    @imported_count = 0
    @errors = []
  end

  def call
    CSV.foreach(@csv_file.path, headers: true).each_slice(@batch_size) do |batch|
      process_batch(batch)
    end

    if @errors.empty?
      Result.new(success: true, data: { imported: @imported_count })
    else
      Result.new(success: false, errors: @errors, data: { imported: @imported_count })
    end
  end

  private

  def process_batch(customers)
    customers.each do |row|
      customer = Customer.new(
        name: row['name'],
        email: row['email'],
        phone: row['phone']
      )

      if customer.save
        @imported_count += 1
      else
        @errors << "Row #{$.}: #{customer.errors.full_messages.join(', ')}"
      end
    end
  end
end

Service Object composition creates complex workflows by combining multiple Service Objects. This pattern maintains single responsibility while supporting multi-step processes. Each Service Object handles one aspect of the overall operation.

class ProcessOrderWorkflow
  def initialize(order_params:, user:)
    @order_params = order_params
    @user = user
  end

  def call
    order_result = CreateOrder.new(params: @order_params, user: @user).call
    return order_result unless order_result.success?

    inventory_result = ReserveInventory.new(order: order_result.data).call
    return inventory_result unless inventory_result.success?

    payment_result = ProcessPayment.new(order: order_result.data).call
    return payment_result unless payment_result.success?

    fulfillment_result = ScheduleFulfillment.new(order: order_result.data).call
    return fulfillment_result unless fulfillment_result.success?

    Result.new(success: true, data: order_result.data)
  end
end

Command pattern variations use Service Objects as commands with undo capabilities. This pattern supports operations that need to be reversed or audited, such as data migrations or administrative actions.

class ChangeUserRole
  def initialize(user:, new_role:)
    @user = user
    @new_role = new_role
    @previous_role = nil
  end

  def call
    @previous_role = @user.role
    @user.update!(role: @new_role)
    
    AuditLog.create!(
      user: @user,
      action: 'role_change',
      details: { from: @previous_role, to: @new_role }
    )
    
    self
  end

  def undo
    return false unless @previous_role

    @user.update!(role: @previous_role)
    
    AuditLog.create!(
      user: @user,
      action: 'role_change_undo',
      details: { from: @new_role, to: @previous_role }
    )
    
    true
  end
end

# Usage with undo capability
command = ChangeUserRole.new(user: user, new_role: 'admin').call
command.undo if error_occurred

Contextual Service Objects accept context objects that provide environmental information, configuration, or cross-cutting concerns. This pattern supports different execution environments without coupling Service Objects to specific frameworks.

class Context
  attr_reader :current_user, :request_id, :environment

  def initialize(current_user:, request_id: SecureRandom.uuid, environment: Rails.env)
    @current_user = current_user
    @request_id = request_id
    @environment = environment
  end
end

class ProcessApiRequest
  def initialize(context:, api_params:)
    @context = context
    @api_params = api_params
  end

  def call
    log_request
    validate_permissions
    
    result = execute_api_logic
    
    log_response(result)
    result
  end

  private

  def log_request
    RequestLogger.log(
      user_id: @context.current_user.id,
      request_id: @context.request_id,
      params: @api_params,
      timestamp: Time.current
    )
  end

  def validate_permissions
    unless @context.current_user.can_access?(@api_params[:resource])
      raise PermissionError, 'Insufficient permissions'
    end
  end

  def execute_api_logic
    # Implementation specific to API operation
  end

  def log_response(result)
    ResponseLogger.log(
      request_id: @context.request_id,
      result: result,
      timestamp: Time.current
    )
  end
end

Testing Strategies

Service Object testing focuses on behavior verification, dependency isolation, and edge case coverage. The testing approach emphasizes unit testing individual Service Objects while supporting integration testing for complex workflows.

Unit testing Service Objects isolates dependencies through mocking and stubbing. This approach tests Service Object logic without external system dependencies, creating fast and reliable tests that focus on business logic verification.

RSpec.describe CreateUserAccount do
  let(:email) { 'test@example.com' }
  let(:password) { 'secure_password' }
  let(:subscription_plan) { 'premium' }
  let(:user) { double('User', id: 1, email: email) }
  let(:subscription) { double('Subscription', id: 1, plan: subscription_plan) }

  describe '#call' do
    before do
      allow(User).to receive(:create!).and_return(user)
      allow(Subscription).to receive(:create!).and_return(subscription)
      allow(UserMailer).to receive(:welcome).and_return(double(deliver_later: true))
    end

    it 'creates user with correct attributes' do
      service = CreateUserAccount.new(
        email: email,
        password: password,
        subscription_plan: subscription_plan
      )

      service.call

      expect(User).to have_received(:create!).with(
        email: email,
        password: password
      )
    end

    it 'creates subscription for user' do
      service = CreateUserAccount.new(
        email: email,
        password: password,
        subscription_plan: subscription_plan
      )

      service.call

      expect(Subscription).to have_received(:create!).with(
        user: user,
        plan: subscription_plan,
        status: 'active'
      )
    end

    it 'sends welcome email' do
      service = CreateUserAccount.new(
        email: email,
        password: password,
        subscription_plan: subscription_plan
      )

      service.call

      expect(UserMailer).to have_received(:welcome).with(email)
    end

    it 'returns user and subscription data' do
      service = CreateUserAccount.new(
        email: email,
        password: password,
        subscription_plan: subscription_plan
      )

      result = service.call

      expect(result).to eq(user: user, subscription: subscription)
    end
  end
end

Error condition testing verifies Service Object behavior during failure scenarios. Tests should cover validation errors, external service failures, and exception handling to ensure robust error handling.

RSpec.describe ProcessPayment do
  let(:order) { double('Order', total: 100.0, update!: true, reload: order) }
  let(:payment_method) { double('PaymentMethod', valid?: true, token: 'token123') }

  describe 'error handling' do
    it 'raises error for invalid payment method' do
      allow(payment_method).to receive(:valid?).and_return(false)
      
      service = ProcessPayment.new(order: order, payment_method: payment_method)
      
      expect { service.call }.to raise_error(ArgumentError, 'Invalid payment method')
    end

    it 'handles payment gateway failures' do
      allow(PaymentGateway).to receive(:charge).and_raise(PaymentGateway::Error.new('Card declined'))
      
      service = ProcessPayment.new(order: order, payment_method: payment_method)
      
      expect { service.call }.to raise_error(PaymentGateway::Error, 'Card declined')
      expect(order).not_to have_received(:update!)
    end

    it 'handles database errors during order update' do
      allow(PaymentGateway).to receive(:charge).and_return(true)
      allow(order).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(order))
      
      service = ProcessPayment.new(order: order, payment_method: payment_method)
      
      expect { service.call }.to raise_error(ActiveRecord::RecordInvalid)
    end
  end
end

Integration testing verifies Service Object interactions with real dependencies, particularly for complex workflows involving multiple Service Objects or external services. These tests run slower but provide confidence in end-to-end functionality.

RSpec.describe ProcessOrderWorkflow, type: :integration do
  let(:user) { create(:user) }
  let(:product) { create(:product, inventory_count: 10) }
  let(:order_params) do
    {
      items: [{ product_id: product.id, quantity: 2 }],
      shipping_address: attributes_for(:address)
    }
  end

  describe 'successful order processing' do
    it 'creates order and processes payment' do
      allow(PaymentGateway).to receive(:charge).and_return(true)
      allow(FulfillmentService).to receive(:schedule).and_return(true)

      workflow = ProcessOrderWorkflow.new(order_params: order_params, user: user)
      result = workflow.call

      expect(result.success?).to be true
      expect(Order.count).to eq(1)
      
      order = Order.last
      expect(order.status).to eq('paid')
      expect(product.reload.inventory_count).to eq(8)
    end

    it 'handles inventory shortage' do
      order_params[:items][0][:quantity] = 15  # More than available

      workflow = ProcessOrderWorkflow.new(order_params: order_params, user: user)
      result = workflow.call

      expect(result.success?).to be false
      expect(result.errors).to include(/insufficient inventory/i)
      expect(Order.count).to eq(0)
    end
  end
end

Test data builders create consistent test data for Service Object testing. These builders generate test objects with appropriate attributes and relationships, reducing test setup complexity.

class OrderBuilder
  def initialize
    @items = []
    @user = build(:user)
    @shipping_address = attributes_for(:address)
  end

  def with_user(user)
    @user = user
    self
  end

  def with_item(product:, quantity: 1)
    @items << { product_id: product.id, quantity: quantity }
    self
  end

  def with_shipping_address(address)
    @shipping_address = address
    self
  end

  def build
    {
      user: @user,
      items: @items,
      shipping_address: @shipping_address
    }
  end
end

# Usage in tests
RSpec.describe ProcessOrderWorkflow do
  let(:user) { create(:user) }
  let(:product) { create(:product) }
  
  it 'processes order with builder' do
    order_data = OrderBuilder.new
      .with_user(user)
      .with_item(product: product, quantity: 2)
      .build

    workflow = ProcessOrderWorkflow.new(order_params: order_data[:items], user: user)
    result = workflow.call

    expect(result.success?).to be true
  end
end

Production Patterns

Production Service Objects require monitoring, logging, error handling, and performance considerations. These patterns ensure Service Objects operate reliably in production environments with appropriate observability and resilience.

Logging within Service Objects provides operational visibility into business operations. Structured logging includes context information, execution timing, and outcome details for monitoring and debugging purposes.

class ProcessSubscriptionRenewal
  def initialize(subscription:)
    @subscription = subscription
    @logger = Rails.logger
  end

  def call
    @logger.info("Starting subscription renewal", subscription_id: @subscription.id)
    
    start_time = Time.current
    
    begin
      validate_renewal_eligibility
      process_payment
      update_subscription
      send_confirmation
      
      duration = Time.current - start_time
      @logger.info(
        "Subscription renewal completed",
        subscription_id: @subscription.id,
        duration_ms: (duration * 1000).to_i
      )
      
      Result.new(success: true, data: @subscription)
    rescue StandardError => error
      duration = Time.current - start_time
      @logger.error(
        "Subscription renewal failed",
        subscription_id: @subscription.id,
        error: error.message,
        duration_ms: (duration * 1000).to_i
      )
      
      Result.new(success: false, errors: [error.message])
    end
  end

  private

  def validate_renewal_eligibility
    unless @subscription.renewable?
      raise RenewalError, "Subscription not eligible for renewal"
    end
    
    @logger.debug("Renewal eligibility validated", subscription_id: @subscription.id)
  end

  def process_payment
    payment_result = PaymentProcessor.charge(
      amount: @subscription.amount,
      customer: @subscription.customer
    )
    
    unless payment_result.success?
      raise PaymentError, "Payment processing failed: #{payment_result.error}"
    end
    
    @logger.debug("Payment processed", subscription_id: @subscription.id)
  end

  def update_subscription
    @subscription.update!(
      renewed_at: Time.current,
      expires_at: 1.month.from_now,
      status: 'active'
    )
    
    @logger.debug("Subscription updated", subscription_id: @subscription.id)
  end

  def send_confirmation
    SubscriptionMailer.renewal_confirmation(@subscription).deliver_later
    @logger.debug("Confirmation sent", subscription_id: @subscription.id)
  end
end

Retry mechanisms handle transient failures in external service interactions. Service Objects implement exponential backoff and circuit breaker patterns to improve resilience against temporary failures.

class ImportDataFromApi
  MAX_RETRIES = 3
  BASE_DELAY = 1.0

  def initialize(api_endpoint:, data_type:)
    @api_endpoint = api_endpoint
    @data_type = data_type
    @retries = 0
  end

  def call
    begin
      fetch_and_process_data
    rescue TransientError => error
      if should_retry?
        sleep(delay_for_retry)
        @retries += 1
        retry
      else
        Rails.logger.error(
          "Max retries exceeded for data import",
          endpoint: @api_endpoint,
          retries: @retries,
          error: error.message
        )
        raise
      end
    end
  end

  private

  def fetch_and_process_data
    Rails.logger.info("Fetching data from API", endpoint: @api_endpoint, attempt: @retries + 1)
    
    response = ApiClient.get(@api_endpoint)
    
    unless response.success?
      raise TransientError, "API request failed: #{response.status}"
    end
    
    process_response_data(response.body)
  end

  def should_retry?
    @retries < MAX_RETRIES
  end

  def delay_for_retry
    BASE_DELAY * (2 ** @retries) + rand(0.1..1.0)
  end

  def process_response_data(data)
    DataImporter.import(@data_type, data)
  end
end

Background job integration allows Service Objects to execute asynchronously. This pattern separates immediate response requirements from long-running operations, improving application responsiveness.

class GenerateLargeReport
  include Sidekiq::Worker
  sidekiq_options queue: :reports, retry: 2

  def perform(user_id, report_params)
    user = User.find(user_id)
    
    ReportGenerationService.new(
      user: user,
      params: report_params.with_indifferent_access
    ).call
  end
end

class ReportGenerationService
  def initialize(user:, params:)
    @user = user
    @params = params
    @logger = Rails.logger
  end

  def call
    @logger.info("Starting report generation", user_id: @user.id, report_type: @params[:type])
    
    report = generate_report
    store_report(report)
    notify_completion
    
    @logger.info("Report generation completed", user_id: @user.id, report_id: report.id)
    
    report
  end

  private

  def generate_report
    case @params[:type]
    when 'sales'
      SalesReportGenerator.new(@user, @params).generate
    when 'analytics'
      AnalyticsReportGenerator.new(@user, @params).generate
    else
      raise ArgumentError, "Unknown report type: #{@params[:type]}"
    end
  end

  def store_report(report)
    ReportStorage.save(
      user_id: @user.id,
      report_data: report.data,
      metadata: report.metadata
    )
  end

  def notify_completion
    ReportMailer.completion_notification(@user).deliver_later
  end
end

Monitoring integration tracks Service Object performance and business metrics. This pattern provides operational dashboards and alerts for business-critical operations.

class ProcessOrderService
  def initialize(order_params:, user:)
    @order_params = order_params
    @user = user
  end

  def call
    StatsD.increment('orders.processing.started')
    start_time = Time.current
    
    begin
      result = execute_order_processing
      
      duration = Time.current - start_time
      StatsD.timing('orders.processing.duration', duration * 1000)
      StatsD.increment('orders.processing.succeeded')
      
      track_business_metrics(result)
      
      result
    rescue StandardError => error
      duration = Time.current - start_time
      StatsD.timing('orders.processing.duration', duration * 1000)
      StatsD.increment('orders.processing.failed', tags: ["error:#{error.class.name}"])
      
      raise
    end
  end

  private

  def execute_order_processing
    # Order processing logic
  end

  def track_business_metrics(result)
    StatsD.increment('orders.created')
    StatsD.histogram('orders.value', result.data.total)
    StatsD.increment('orders.by_user_type', tags: ["user_type:#{@user.user_type}"])
  end
end

Common Pitfalls

Service Objects present common misuse patterns that reduce code quality and maintainability. Understanding these pitfalls helps developers implement Service Objects effectively while avoiding architectural problems.

God Object antipattern occurs when Service Objects accumulate too many responsibilities. This pattern violates single responsibility principles and creates complex, difficult-to-test classes that become bottlenecks for changes.

# Problematic: God Object Service
class UserAccountManager
  def initialize(user:)
    @user = user
  end

  def create_account(params)
    # Account creation logic
  end

  def update_profile(params)
    # Profile update logic
  end

  def change_password(old_password, new_password)
    # Password change logic
  end

  def process_subscription(plan)
    # Subscription processing
  end

  def send_notifications(type)
    # Notification sending
  end

  def generate_reports(type)
    # Report generation
  end

  def handle_billing(action)
    # Billing operations
  end
end

# Better: Focused Service Objects
class CreateUserAccount
  def initialize(params:)
    @params = params
  end

  def call
    # Focused account creation logic
  end
end

class UpdateUserProfile
  def initialize(user:, params:)
    @user = user
    @params = params
  end

  def call
    # Focused profile update logic
  end
end

class ChangeUserPassword
  def initialize(user:, old_password:, new_password:)
    @user = user
    @old_password = old_password
    @new_password = new_password
  end

  def call
    # Focused password change logic
  end
end

Anemic Service Objects lack business logic and simply delegate to other objects without adding value. This pattern creates unnecessary indirection without providing architectural benefits.

# Problematic: Anemic Service
class UpdateUser
  def initialize(user:, params:)
    @user = user
    @params = params
  end

  def call
    @user.update!(@params)
  end
end

# Better: Service with actual business logic
class UpdateUserProfile
  def initialize(user:, params:)
    @user = user
    @params = params
  end

  def call
    validate_profile_changes
    previous_email = @user.email
    
    @user.update!(@params)
    
    if email_changed?(previous_email)
      send_email_verification
      log_email_change(previous_email)
    end
    
    @user
  end

  private

  def validate_profile_changes
    if @params[:email] && !email_format_valid?(@params[:email])
      raise ValidationError, 'Invalid email format'
    end
  end

  def email_changed?(previous_email)
    previous_email != @user.email
  end

  def send_email_verification
    UserMailer.email_verification(@user).deliver_later
  end

  def log_email_change(previous_email)
    AuditLog.create!(
      user: @user,
      action: 'email_changed',
      details: { from: previous_email, to: @user.email }
    )
  end

  def email_format_valid?(email)
    email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end
end

Inconsistent error handling across Service Objects creates unpredictable behavior for consumers. Service Objects within an application should follow consistent error handling patterns.

# Problematic: Inconsistent error handling
class ServiceA
  def call
    return false if invalid_input?
    process_data
    true
  end
end

class ServiceB
  def call
    raise ArgumentError, 'Invalid input' if invalid_input?
    process_data
  end
end

class ServiceC
  def call
    if invalid_input?
      { success: false, error: 'Invalid input' }
    else
      { success: true, data: process_data }
    end
  end
end

# Better: Consistent error handling with Result objects
class BaseService
  def call
    validate_inputs
    data = execute
    Result.new(success: true, data: data)
  rescue ValidationError => e
    Result.new(success: false, errors: [e.message])
  rescue StandardError => e
    Rails.logger.error("Service error: #{e.message}", service: self.class.name)
    Result.new(success: false, errors: ['An unexpected error occurred'])
  end

  protected

  def validate_inputs
    # Override in subclasses
  end

  def execute
    # Override in subclasses
  end
end

class ProcessPayment < BaseService
  def initialize(order:, payment_method:)
    @order = order
    @payment_method = payment_method
  end

  protected

  def validate_inputs
    raise ValidationError, 'Order required' unless @order
    raise ValidationError, 'Payment method required' unless @payment_method
  end

  def execute
    # Payment processing logic
  end
end

Tight coupling to framework dependencies makes Service Objects difficult to test and reuse. Service Objects should accept dependencies through injection rather than accessing global state directly.

# Problematic: Tight framework coupling
class ProcessOrder
  def call
    user = User.find(params[:user_id])  # Direct ActiveRecord access
    
    if Rails.env.production?  # Direct Rails environment access
      PaymentGateway.charge(user.card)  # Direct external service access
    end
    
    ActionCable.server.broadcast('orders', data)  # Direct ActionCable access
  end
end

# Better: Dependency injection
class ProcessOrder
  def initialize(order_repository:, payment_service:, broadcaster:, environment:)
    @order_repository = order_repository
    @payment_service = payment_service
    @broadcaster = broadcaster
    @environment = environment
  end

  def call(user_id:, order_params:)
    user = @order_repository.find_user(user_id)
    
    if @environment.production?
      @payment_service.charge(user.card)
    end
    
    @broadcaster.broadcast('orders', order_data)
  end
end

# Usage with dependency injection
service = ProcessOrder.new(
  order_repository: OrderRepository.new,
  payment_service: PaymentService.new,
  broadcaster: OrderBroadcaster.new,
  environment: ApplicationEnvironment.new
)

service.call(user_id: 1, order_params: params)

Lack of input sanitization in Service Objects creates security vulnerabilities and data integrity issues. Service Objects should validate and sanitize all input parameters before processing.

# Problematic: No input validation
class CreateProduct
  def initialize(params:)
    @params = params
  end

  def call
    Product.create!(@params)  # Direct parameter usage
  end
end

# Better: Input validation and sanitization
class CreateProduct
  ALLOWED_PARAMS = [:name, :description, :price, :category_id].freeze

  def initialize(params:, current_user:)
    @params = params
    @current_user = current_user
  end

  def call
    validate_permissions
    sanitized_params = sanitize_params
    validate_business_rules(sanitized_params)
    
    Product.create!(sanitized_params)
  end

  private

  def validate_permissions
    unless @current_user.can?(:create, Product)
      raise PermissionError, 'Insufficient permissions to create product'
    end
  end

  def sanitize_params
    @params.slice(*ALLOWED_PARAMS).tap do |clean_params|
      clean_params[:name] = clean_params[:name].to_s.strip if clean_params[:name]
      clean_params[:description] = sanitize_html(clean_params[:description]) if clean_params[:description]
      clean_params[:price] = validate_price(clean_params[:price]) if clean_params[:price]
    end
  end

  def validate_business_rules(params)
    raise ValidationError, 'Name required' if params[:name].blank?
    raise ValidationError, 'Price must be positive' if params[:price] && params[:price] <= 0
  end

  def sanitize_html(text)
    ActionView::Base.full_sanitizer.sanitize(text)
  end

  def validate_price(price)
    Float(price)
  rescue ArgumentError
    raise ValidationError, 'Invalid price format'
  end
end

Reference

Core Components

Component Purpose Implementation
Service Object Encapsulates business logic Class with #call method
Result Object Structured return values Success/failure state with data/errors
Context Object Environmental information Configuration and cross-cutting concerns

Common Method Names

Method Usage Convention
call Primary execution method Most common interface
perform Background job compatibility Sidekiq/Resque integration
execute Alternative execution method Command pattern variation
run Simple execution Minimal Service Objects

Initialization Patterns

# Dependency injection
def initialize(user:, payment_service:, logger: Rails.logger)

# Parameter object
def initialize(params:)

# Mixed approach
def initialize(user:, order_params:, options: {})

# Class method shortcut
def self.call(user:, params:)
  new(user: user, params: params).call
end

Return Value Patterns

Pattern Use Case Example
Boolean Simple success/failure true/false
Object Created/modified entity user
Hash Multiple values { user: user, token: token }
Result Object Structured response Result.new(success: true, data: user)
Self Command pattern self for method chaining

Error Handling Strategies

Strategy Implementation Use Case
Exception raising raise ValidationError Exceptional conditions
Result objects Result.new(success: false) Expected failures
Boolean returns return false Simple success/failure
Nil returns return nil Optional operations

Testing Approaches

# Unit testing with mocks
RSpec.describe MyService do
  let(:dependency) { double('Dependency') }
  
  before do
    allow(ExternalService).to receive(:call).and_return(result)
  end
  
  it 'calls external service' do
    service.call
    expect(ExternalService).to have_received(:call)
  end
end

# Integration testing
RSpec.describe MyService, type: :integration do
  it 'creates records in database' do
    expect { service.call }.to change(Model, :count).by(1)
  end
end

Production Monitoring

# Logging pattern
Rails.logger.info("Operation started", service: self.class.name, params: @params)

# Metrics pattern
StatsD.increment('service.execution.started')
StatsD.timing('service.execution.duration', duration)

# Error tracking
Bugsnag.notify(error) do |report|
  report.add_tab(:service, {
    class: self.class.name,
    params: @params
  })
end

Common Validation Patterns

Validation Type Implementation Purpose
Presence raise ArgumentError if param.nil? Required parameters
Format raise ArgumentError unless email.match?(regex) Data format validation
Business rules raise ValidationError unless user.can_perform? Domain constraints
State validation raise StateError unless record.valid_state? Object state checks

Performance Considerations

Concern Approach Implementation
Database queries Eager loading includes(:associations)
External API calls Caching Rails.cache.fetch(key) { api_call }
Large datasets Batch processing find_each or in_batches
Memory usage Streaming CSV.foreach instead of CSV.read