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.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
<form data-controller="query-sync" data-action="change->query-sync#apply">
<select name="status" class="rounded border p-2">
<option value="">Any</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
Filter UI that syncs query params via Stimulus (no front-end router)
Share this code
Here's the card — post it anywhere.