Rails Services

Rails is an MVC framework. In small applications logic can be split between the model, view and controller layers. As applications grow many developers choose to introduce a service layer. The benefit of adding a service layer is that it can help you control your fat models.

What are the benefits of slimming your models?

A class should have single responsibility. Classes that have single responsibility are easier to test, easier to reuse and easier to maintain. If a class is 1000 lines long then it probably doesn’t adhere to the single responsibility principle.

Another benefit of slimming down your models and moving business logic into services is that you are less coupled to rails. You can more easily pull your service logic out into another application, should you wish to do so.

Okay. Tell me how I can use services..

If you agree with the advantages of services then the next obvious question is; what should you put into services?

Rails models can become a bit of a dumping ground for business logic, queries, validations, relationships, database attributes etc. Services allow you to write purer entity objects. A model should represent just an entity (a Person, or an Address). It should contain validations, relations and methods related to that entity. Any logic that deals with multiple models should go in a service.

The logic in your services directory is really you core business logic. It is the way that all of your model objects are wired together to create useful behaviour for the user.

Lets look at an example:

class Address < ActiveRecord::Base
  validates :first_line, presence: true

  def full_address
    full_address_array.select(&:present).join("n")
  end

  def full_address_array
    [
      first_line,
      second_line,
      town,
      county,
      postcode
    ]
  end
end

Here is a simple Address model. The model has a number of attributes that make up a full address.

Now imagine that we want to add some functionality to check whether an address already exists. One option would be to write a SQL query or Rails scope. Another option is to put the logic into a service.

class AddressService
  def self.find_or_create(address)
    existing = existing_address(address)
    return existing if existing.present?

    address.save
    address
  end

  def self.existing_address(address)
    Address.find_by(
      first_line: address.first_line,
      second_line: address.second_line,
      town: address.town,
      county: address.county,
      postcode: address.postcode
    )
  end
end

Now there is a logical separation between logic that is specific to the entity and logic that deals with collections or other models.