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.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
Value objects for domain modeling
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
Share this code
Here's the card — post it anywhere.