DEV Community

Cover image for How to improve and standardize writing services in Ruby project
Anton
Anton

Posted on • Edited on

How to improve and standardize writing services in Ruby project

In this short note, I want to share with you a way to create services of any complexity in Ruby projects. This will standardize all services and provide a unified approach to their development.

Ruby Gem

Everything to know about Servactory gem.
Repository in GitHub: github.com/servactory/servactory
Documentation: servactory.com

The library was developed on the basis of several projects and includes everything you need to implement services of any complexity.

Usage

There is an example of a service implementation for sending events. This implementation includes a service for working with the API client and a service for preparing data.

Service for working with the API client, which makes a request to an external service:

class AmplitudeService::Base < ApplicationService::Base
  perform :api_request!

  private

  def perform_api_request!
    outputs.response = api_request
  rescue AmplitudeApi::Errors::Failed => e
    fail!(message: e.message)
  end

  def api_request
    raise "Need to specify the API request"
  end

  def api_model
    raise "Need to specify the API model"
  end

  def api_client
    @api_client ||= AmplitudeApi::Client.new
  end
end
Enter fullscreen mode Exit fullscreen mode
class AmplitudeService::TrackEvent < AmplitudeService::Base
  input :user_identifier, type: String
  input :device_identifier, type: String, required: false, default: nil
  input :type, type: Symbol

  input :event_properties, type: Hash, required: false, default: {}
  input :user_properties, type: Hash, required: false, default: {}

  # some other inputs

  output :response, type: AmplitudeApi::Responses::Event

  private

  def api_request
    api_client.events.track(api_model)
  end

  def api_model
    AmplitudeApi::Requests::Event.new(
      user_identifier: inputs.user_identifier,
      device_identifier: inputs.device_identifier,
      type: inputs.type,
      event_properties: inputs.event_properties,
      user_properties: inputs.user_properties,
      # some other data
      time:
    )
  end

  def time
    DateTime.now.strftime("%Q").to_i
  end
end
Enter fullscreen mode Exit fullscreen mode

And directly the service itself, which prepares the data and passes it on to fulfill the request:

class UserService::Amplitude::Activated < ApplicationService::Base
  input :user, type: User

  make :api_request!

  private

  def api_request!
    service_result = AmplitudeService::TrackEvent.call(
      user_identifier: inputs.user.id,
      type: :USER_ACTIVATED,
      event_properties: {
        # some metadata
      },
      user_properties: {
        # some metadata
      }
    # some other data
    )

    fail!(error: service_result.error) if service_result.failure?
  end
end
Enter fullscreen mode Exit fullscreen mode

The service is called like this:

UserService::Amplitude::Activated.call!(user: User.first)
Enter fullscreen mode Exit fullscreen mode

The Servactory has many utilities that will allow you to build services with much more complex logic, but keeping them simple and expressive. Make sure of it by reading the documentation.

Testing

As far as testing is concerned, this is implemented by regular Ruby class testing.

You can do regression testing on input, checking for expected falls. You can check expected output values in Result.

Same approach to the development of services makes their testing also come to a single form!

Top comments (0)