module Posts
class CreateService
def initialize(author:, params:)
@author = author
@params = params
end
def call
ActiveRecord::Base.transaction do
create_post
attach_images if @params[:images].present?
notify_followers
schedule_publication if @post.scheduled?
Result.success(post: @post)
end
rescue ActiveRecord::RecordInvalid => e
Result.failure(errors: e.record.errors.full_messages)
rescue => e
Rails.logger.error "Failed to create post: #{e.message}"
Result.failure(errors: ['An unexpected error occurred'])
end
private
def create_post
@post = @author.posts.create!(
title: @params[:title],
body: @params[:body],
status: @params[:status] || 'draft'
)
end
def attach_images
@params[:images].each do |blob_id|
@post.images.attach(blob_id)
end
end
def notify_followers
return unless @post.published?
NotifyFollowersJob.perform_later(@post.id)
end
def schedule_publication
PublishPostJob.set(wait_until: @post.publish_at).perform_later(@post.id)
end
end
class Result
attr_reader :data, :errors
def self.success(data = {})
new(success: true, data: data)
end
def self.failure(errors:)
new(success: false, errors: errors)
end
def initialize(success:, data: {}, errors: [])
@success = success
@data = data
@errors = errors
end
def success?
@success
end
def failure?
!@success
end
end
end
module Api
module V1
class PostsController < ApplicationController
def create
result = Posts::CreateService.new(
author: current_user,
params: post_params
).call
if result.success?
render json: result.data[:post],
serializer: PostSerializer,
status: :created
else
render json: { errors: result.errors },
status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :body, :status, images: [])
end
end
end
end
Service objects encapsulate complex business logic that doesn't belong in models or controllers. Each service performs one operation, like creating a post with side effects, processing a payment, or importing data. I create services in app/services with a single public call method. Services return result objects indicating success/failure with data or errors. This pattern keeps controllers thin—they orchestrate but don't implement business logic. Services are easily testable in isolation and reusable across controllers, jobs, and rake tasks. For multi-step operations, I chain services or use saga patterns. This architecture scales well as applications grow.