class UserRegistrationService
class Result
attr_reader :user, :errors
def initialize(success:, user: nil, errors: [])
@success = success
@user = user
@errors = errors
end
def success?
@success
end
def failure?
!@success
end
end
def initialize(user_params, notifier: UserNotifier.new)
@user_params = user_params
@notifier = notifier
end
def call
user = User.new(@user_params)
if user.save
create_user_profile(user)
send_welcome_email(user)
track_registration(user)
Result.new(success: true, user: user)
else
Result.new(success: false, errors: user.errors.full_messages)
end
rescue StandardError => e
Result.new(success: false, errors: [e.message])
end
private
def create_user_profile(user)
user.create_profile(
bio: 'New user',
avatar_url: default_avatar_url
)
end
def send_welcome_email(user)
@notifier.send_welcome_email(user)
end
def track_registration(user)
Analytics.track(
user_id: user.id,
event: 'user_registered',
properties: { email: user.email }
)
end
def default_avatar_url
'https://example.com/default-avatar.png'
end
end
# Usage in controller:
# result = UserRegistrationService.new(user_params).call
#
# if result.success?
# redirect_to result.user
# else
# flash[:error] = result.errors.join(', ')
# render :new
# end
class PaymentProcessorService
def initialize(order, payment_gateway: StripeGateway.new)
@order = order
@payment_gateway = payment_gateway
end
def call
ActiveRecord::Base.transaction do
charge_customer
update_order_status
send_receipt
fulfill_order
end
{ success: true, order: @order }
rescue PaymentError => e
handle_payment_failure(e)
{ success: false, error: e.message }
end
private
def charge_customer
@payment_gateway.charge(
amount: @order.total_amount,
currency: @order.currency,
customer: @order.user.stripe_customer_id
)
end
def update_order_status
@order.update!(
status: 'paid',
paid_at: Time.current
)
end
def send_receipt
OrderMailer.receipt(@order).deliver_later
end
def fulfill_order
OrderFulfillmentJob.perform_later(@order.id)
end
def handle_payment_failure(error)
@order.update!(
status: 'payment_failed',
payment_error: error.message
)
OrderMailer.payment_failed(@order).deliver_later
end
end
Service objects extract complex business logic from models and controllers, following Single Responsibility Principle. I structure services with a clear public interface—typically a call method. Services handle multi-step operations, external API calls, or complex workflows. They're testable in isolation and reusable across contexts. I organize services in app/services by domain. Services accept dependencies via initializer, enabling dependency injection. Return values use result objects or monads for explicit success/failure handling. Services keep controllers thin and models focused on persistence. This pattern scales well as applications grow, maintaining clean separation of concerns. Well-designed services read like domain language, improving code comprehension.