Service objects for business logic encapsulation

Sarah Mitchell Feb 2026
2 tabs
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
2 files · ruby Explain with highlit

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.