There are many good reasons to use Service Objects. For example to encapsulate tasks which work on multiple models, does one specific task and doesn’t really fit in as a class method on any one model which its works on. There are many articles talking about how/when to use them and there are so many ways of building a class and calling it a service object.

How do we decide what is a well formed service object and what should go in it? I mean anything form a simple PORO, to a subclass of some other complex class which has a perform method could be argued to be a service object. So what is the convention when using service objects.

My opinion

Placement & Name

Put them in /app/services/ Update application.rb to load them config.autoload_paths += ["#{config.root}/app/services}"] Name them like a verb/action, with a *_service.rb in the end. e.g. analyze_user_points_service.rb. It should be named like a method (analyze_user_points_service.rb and not like a Class (UserPointsAnalyzerService).

  class AnalayzeUserPointsService

  end

Return Value

Tells success & failure and is also a Data Transfer Object. I like to have one class named ServiceResult. The result here is immutable as the instances only have attr_reader.

class ServiceResult

  attr_reader :status, :message, :data, :errors

  def initialize(status:, message: nil, data: nil, errors: [])
    @status = status
    @message = message
    @data = data
    @errors = errors
  end

  def success?
    status == true
  end

  def failure?
    !success?
  end

  def has_data?
    data.present?
  end

  def has_errors?
    errors.present? && errors.length > 0
  end

  def to_s
    "#{success? ? 'Success!' : 'Failure!'} - #{message} - #{data}"
  end

end

Error

Errors should be caught and failure result should be returned. I think that the service when errors out to do something in essense has failed to do it. So an error should be recorded as a failure error. At the smae time the error should be recorded somewhere so it can be triaged later.

In some rare cases like when using them in background jobs, we need to raise an error from the service so the job can be retried or the error can be recorded. Raising the error would be an exception to the convention here.

def call
  # do whatever first
  return result
rescue => e
  Bugsnag.notify
  puts "got error #{e}"
  return result
end

Public Action Method

This is the one and only publically exposed method. I like calling it call as that’s what Lambda’s and proc’s also take. I think perform is also an accetable name. My resque jobs have that name so it sits well with me.

def call
  # do all stuff here and call private methods from here
end

Factory Method

Having a build method makes a lot of things better. With build we are doing DI. This gives more flexibility and less coupling. The class’es new method is getting all dependecies built in the build and passed. The new just sets the instances and moves on. Testing is easier coz we can setup different dependecies and pass it in. The service is now immutable. Everytime build is called it creates a new instance.

def self.build(u_id)
  # here User is a dependecy managed here
  # also user is a dependency build and then injected
  user = User.find u_id
  self.new(user
end

def initialize(user)
  # see how user the dependency is being injected
  @user = user
end

Refernces