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 |