we develop communication with weLaika Advertising

Service Objects in Rails

Tags: ruby on rails, ruby, poro, service object
Fabrizio Monti -
Fabriziomonti

Enough has been written already about service objects in Rails. They help us to keep our controllers skinny, to separate business logic and to test more easily.

We can identify two types of service objects: - “Fire and forget” ones. We execute them, but we don’t care about their return value or if they failed. For instance: a service object which sends a welcome email to a new user. - Service with an outcome. We want to known if they succeeded or not, their return value and if there was any error.

The latter were giving me headaches. Should they return a hash object? If so, which keys should I use to maintain consistency between all projects? Should they return a struct object instead?

That’s why we tried to standardise our service objects with this small gem: https://github.com/welaika/simpleserviceobject

Let’s see an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class LendMoney < SimpleServiceObject::Base
  def call(lender, borrower, amount)
    if lender.balance >= amount
      lender.withdrawal(amount)
      borrower.deposit(amount)
    else
      errors.push("#{lender.name} has not enough money to borrow!")
    end

    generate_transaction_id
  end

  def generate_transaction_id
    SecureRandom.uuid
  end
end

Let’s suppose Alice balance is only € 200.

1
2
3
4
5
result = LendMoney.call(alice, bob, 500.00)
result.success? # => false, because `#errors` contains errors
result.failure? # => true
result.errors # => ["Alice has not enough money to borrow!"]
result.value = "2d931510-d99f-494a-8c67-87feb05e1594" # It's the return value of `#call`

Now let’s suppose Alice balance is € 700.

1
2
3
4
5
result = LendMoney.call(alice, bob, 500.00)
result.success? # => true, because `#errors` does not contain errors
result.failure? # => false
result.errors # => []
result.value = "bad85eb9-0713-4da7-8d36-07a8e4b00eab" # It's the return value of `#call`

This simple interface is enough for our typical service object.

simple_service_object gem is so small that you can copy/paste the code in your project and change it to suit your needs.

If you are interested in a fully fledged gem with arguments validation, callbacks and service objects composition, then take a look at active_interaction gem.