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 a way to create services of any complexity in Ruby projects. This approach standardizes all services and provides a unified pattern for their development.

Ruby Gem

Repository: github.com/servactory/servactory
Documentation: servactory.com

Servactory was built on the basis of several real-world projects and includes everything you need to implement services of any complexity.

Usage

Here is an example of a service composition for tracking events via an external API. It includes a base service for API communication, a specific service for sending events, and a high-level service that prepares data and delegates the request.

Base service for working with the API client:

class Amplitude::API::Base < ApplicationService::Base
  make :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

Service that sends a specific event:

class Amplitude::API::Events::Track < Amplitude::API::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

High-level service that prepares the data and delegates the tracking:

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

  make :track!

  private

  def track!
    service_result = Amplitude::API::Events::Track.call(
      user_identifier: inputs.user.id,
      type: :USER_ACTIVATED,
      event_properties: {
        # some metadata
      },
      user_properties: {
        # some metadata
      }
      # some other data
    )

    fail_result!(service_result) if service_result.failure?
  end
end
Enter fullscreen mode Exit fullscreen mode

Calling the service:

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

Servactory provides many utilities that allow you to build services with much more complex logic while keeping them simple and expressive. Explore the documentation to learn more.

Testing

Testing Servactory services follows the same approach as testing regular Ruby classes. You can write regression tests for input validations, checking for expected failures. You can verify expected output values in the Result object.

A unified approach to building services naturally leads to a unified approach to testing them.

Top comments (0)