How to improve and standardize writing services in Ruby project
Tags: ruby, rails, services
Published: 2023-06-05 09:33:20

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](https://github.com/servactory/servactory)
Documentation: [servactory.com](https://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:

```ruby
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
```

```ruby
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
```

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

```ruby
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
```

The service is called like this:

```ruby
UserService::Amplitude::Activated.call!(user: User.first)
```

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](https://servactory.com).

## 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!