Service Objects in Rails

2016-02-07

Let’s kick off 2016 with a whistle-stop tour of one of my favourite OO approaches, Service Objects, in the context of Rails.

In Fowler’s incredible ‘Patterns of Enterprise Application Architecture’, he identifies the need of a service layer, that mediates between the domain model, and elements that need to interface with the business logic of the application. Unfortunately, Rails’ straight up MVC architecture doesn’t provide ‘out-of-the-box’ support for services - it expects your business logic to exist within your models or controllers (the latter being considered an anti-pattern). We can implement this service layer through service objects.

To begin with, let’s start out by defining what a service is and isn’t, and then look at how I like to implement service objects in Rails. Service objects are a hotly debated thing inside the Rails community, so fair warning, this is going to be opinionated.

What is (and isn’t) a Service Object

Programming is a relatively new field, with web and enterprise development even younger, so we don’t have a particualrly well defined canon of work that forms the vocabularly we use to discuss conscepts. As a result, service objects have been defined over and over, with little agreement. What follows is how I define a service object.

What Service Objects are not

Let’s start by defining what service objects are not, before giving them a positive description.

1. Methods

In an object-oriented world, we tend to think of ‘things’ as objects and how we act on ‘things’ as methods (or whatever word that describes a procedure acting on an object). A service is not a method: it is an action represented by an object, for example RegisterUser or SendReportEmail. The service handles the mediation between one or more objects, so isn’t part of another object’s behaviour.

2. Query/Finder objects

In my opinion, service objects are distinct from query objects, notably in that they have no return value. This means a service only deals with an action or actions, and only returns an exception when one is raised. The service is a bit of a diva, so will not be answering questions at this time.

This is in stark contrast to a query object, which returns records from a persistance store of some sort (RDBMS, document stores, etc).

3. The kitchen sink

Services can often become a dumping ground for many things, and can lose sight of their true purpose. i.e. if it isn’t a model, a controller or a view, it must be a service! They can become bloated, and a single service can end up doing too many things. I take a fairly strict view on how much a service can do, and what services encapsulate, but more on that in a bit.

What Service Objects are

1. Architecture independent

Service objects are an implementation of a specific pattern, usually the command pattern, and therefore are architecture and language indepdent. In Rails, I think it is preferable to implement services as Plain Old Ruby Objects, or ‘PORO’. The PORO is the secret weapon of a Rails application, hiding in plain sight (or at least the lib/ directory).

2. Simple

Service objects are, in my opinion, best as simple objects. Without getting too bogged down in implementation detail, I believe they should have a single entry point, and do exactly one thing: the name of the service.

3. Loosely coupled

Dependency injection is used with service objects to make sure they are loosely coupled. The service itself should have little to no bearing on the rest of the application, and ideally should not inherit from other objects. This has the side effect of being blazing fast to test, as you only need to set up the client object (as a test mock or the like).

4. Obvious

I think this could probably be a ‘Rule #0’. It should be obvious what a service does from the name of class. NotifyUser sends a notification the the user, UpdateOrder updates an order. Not only is this readable to whoever is reading the codebase, it helps immensely when debugging as the stack trace will make business sense, too.

Implementation

On to the nitty-gritty - how do we implement these things anyway? We’ve set some ground rules above for services, but there are two addenda:

Services do not call other services

This is a pretty huge (and controversial) restriction. The reason for this is that a service that calls another service means it no longer has a single responsibility - it’s doing that service’s job too by calling it. I prefer to orchestrate these services inside a ‘flow’ or ‘use case’.

For example, let’s say that when a user registers with our application, we send out a welcome email and set up some default preferences. The two services, SendWelcomeEmail and SetUserDefaults respectively, are called from within a RegisterUser flow. Flows are powerful tools for domain modelling, but a little out of scope here.

Services live in lib/

This comes down to personal preference, but I think services are most at home inside lib/ rather than app/. The reason for this they behave almost like libraries - they don’t depend on the rest of the application to function. Hypothetically, a collection of services could be extracted and plugged into another app without any problems, as long as you wanted the same logic.

Examples

Here’s an example of a simple service that sends a welcome email to a user.

class SendWelcomeEmail
  attr_reader :user
  def self.call(options = {})
    new(options).call
  end

  def initialize(options = {})
    @user = options.fetch(:user)
  end

  def call
    unless user.has_been_welcomed
      send_welcome_email to: user
    end
  end

  private

  # ...
end

So what we have is a class that has a single entry point in call, both as an instance and class method. We would usually go for SendWelcomeEmail.call user: new_user when the service is called. It does exactly one thing: sends the welcome email after checking if the user has previously been welcomed. The service is completely independent of the rest of the app, and easy to test:

describe SendWelcomeEmail do
  subject { described_class }

  describe '.call' do
    context 'non welcomed user' do
      let(:user) { double 'user', has_been_welcomed: false }
      it 'sends an email' do
        expect_any_instance_of(subject).to receive(:send_welcome_email)
        subject.call(user: user)
      end
    end

    context 'welcomed user' do
      let(:user) { double 'user', has_been_welcomed: true }
      it 'does not send an email' do
        expect_any_instance_of(subject).not_to receive(:send_welcome_email)
        subject.call(user: user)
      end
    end
  end
end

Caveats

It is easy to go overboard with services, falling into a pattern of extracting business logic into more and more services. Services are best in a complex application with a lot of business logic that requires testing and validation. It’s totally fine to have your business logic in a model, but the power of services and orchestrating them into flows really shines where you are modelling complex real world problems, such as invoicing, lead generation and resource management.

Conclusion

I think that service objects are an excellent way of modelling your domain(s): real-world processes can be broken down into steps, which are written as services and orchestrated in flows.

This allows the application to tell the developers what the behaviour is without having to deal with endless callbacks and tightly coupled models; letting the models act as a pure persistance layer. Controllers can call services directly to slim them down, or call flows if the action itself requires a chain of events to occur.

Ruby is an immensely expressive language with perhaps the best object-oriented heritage going, and by leveraging good OO practices we can make our Rails apps more testable, more adaptable and more maintainable.