ruby 50 lines · 1 tab

Service objects for complex business logic

Alex Kumar Jan 2026
1 tab
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
1 file · ruby Explain with highlit

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

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
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

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
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

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<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)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Service objects for complex business logic — share card
Link copied