class CreatePostService
def initialize(author:, params:)
@author = author
@params = params
end
def call
post = nil
ActiveRecord::Base.transaction do
post = @author.posts.create!(@params)
# Notify followers in background
NotifyFollowersWorker.perform_async(post.id) if post.published?
# Update author stats
@author.increment!(:posts_count)
end
Result.success(post: post)
rescue ActiveRecord::RecordInvalid => e
Result.failure(errors: e.record.errors)
rescue StandardError => e
Rails.logger.error("CreatePostService failed: #{e.message}")
Result.failure(error: 'INTERNAL_ERROR')
end
class Result
attr_reader :post, :errors, :error
def initialize(success:, post: nil, errors: nil, error: nil)
@success = success
@post = post
@errors = errors
@error = error
end
def success?
@success
end
def self.success(post:)
new(success: true, post: post)
end
def self.failure(errors: nil, error: nil)
new(success: false, errors: errors, error: error)
end
end
end
As business logic grows, controllers become bloated with transaction management, error handling, and cross-model orchestration. Service objects extract this complexity into dedicated classes with a single public method (usually call), keeping controllers thin and focused on HTTP concerns. Each service object represents one business operation like CreateOrder or ProcessRefund. I pass dependencies explicitly rather than hiding them in class methods, which makes testing straightforward—I can stub external services without touching global state. Service objects also centralize logging and metrics collection for business-critical operations. The pattern works best when each service has a clear input contract and returns a result object indicating success or failure with relevant data.