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
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
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
Calling the service:
Users::Amplitude::Activated::Track.call!(user: User.first)
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)