How and Why to Use Service Objects in Rails

If you’re following trends in Rails you’ve probably heard the word ‘service’ being tossed around. I was surprised to find that even though there seems to be consensus that service objects are a good idea, there is no standard pattern for building or consuming them.

Why Service Objects

Let’s start by looking at the problem that service objects try to solve. Here is a common Rails controller that creates Users.

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    user = User.new(create_user_params)
    if user.save
      redirect_to root_path
    else
      @user = user
      render :new
    end
  end

  private

  def create_user_params
    params.require(:user).permit(:name, :email)
  end
end

This should hopefully look fairly standard. Let’s introduce a common feature - we want to send a welcome email when a user is created. So here’s the crux of the matter - where does this code go? If you’re following the standard Rails mantra of ‘Fat models, Skinny controllers’ you would probably lean towards doing this inside the model.

class User < ActiveRecord::Base
  after_create :send_welcome_email

  private

  def send_welcome_email
    UserMailer.delay.welcome(self)
  end
end

This works, but this code is rather problematic:

  1. This code breaks the Single Responsibility Principle (SRP). This design principle (the ‘S’ in the popular SOLID design principles says that a class should only have a single responsibility. While this is not the only principle to consider, SRP helps in designing clean and maintainable objects. To approach this from another angle, just look at the size of your user.rb model - in most Rails applications this model is well over 500 lines, often 1000 lines or more. You don’t need an architect to tell you that this is a maintenance headache.
  2. Creating a user and sending the welcome email is now tied together. Forever. If we ever wanted to create a user without sending this email we can’t - for example, if we wanted to create users in an admin interface or via some import task we can’t do so without sending welcome emails to users. This is also problematic for testing - every single feature test in our application will probably require at least one user to be exist, which will now in turn send an email as well, slowing down the test suite and adding additional noise.
  3. The User model now has a dependency on the UserMailer and the UserMailer has a dependency on the User model - this tighly coupling is another code smell.

None of these problems are particularly scary by themselves, but tend to become really troublesome as your application grows. At some point we might want to add analytics around creating a user, send information to a data warehouse or create an account in a CRM like Intercom or Desk. As your application grows the models tend to get bloated and cluttered with external dependencies.

So what’s the alternative? One approach would be to push this functionality back into the controller. This solves the problem of bloated models, but now we end up with bloated controllers instead. Also, if we have multiple endpoints that perform similar functions (for example, if we added a mobile API endpoint to create users or created an admin endpoint to create users in bulk) we will end up with duplication. The better approach is to use service objects.

How to Use Service Objects

Service objects live between the controller layer and the model layer. These objecs are usually responsible for coordinating the different pieces of logic associated with a specific use case or feature.

As we’re designing service objects we need to keep in mind the requirements of the consuming object - the UsersController in this case. The controller needs to know if the creation of the user object succeeded and needs access to the user object itself (in order to display validation errors, which are tied to the model, as well as binding previously entered values to the form). A solution which hides the user object from the controller is not going to work.

So what do we want the interaction with the service to look like? And how do we name these services? This is an area with plently of healthy debate. For now, let’s just call this UserService and circle back to that question. Here is one possible implementation of the UsersController#create action, using a service object. (Note that the rest of the controller class has been excluded for brevity, but will look exactly like the original example)

class UsersController < ApplicationController
  def create
    service = UserService.new
    if service.create_user(create_user_params)
      redirect_to root_path
    else
      @user = service.user
      render :new
    end
  end
end

Here’s another possible way to consume the service.

class UsersController < ApplicationController
  def create
    service = UserService.new
    result = service.create_user(create_user_params)
    if result.success?
      redirect_to root_path
    else
      @user = result.user
      render :new
    end
  end
end

And another.

class UsersController < ApplicationController
  def create
    result = UserService.create_user(create_user_params)
    if result.success?
      redirect_to root_path
    else
      @user = result.user
      render :new
    end
  end
end

Yet another.

class UsersController < ApplicationController
  def create
    user = UserService.create_user(create_user_params)
    if user.valid?
      redirect_to root_path
    else
      @user = user
      render :new
    end
  end
end

All of these are legitimate options. Testing the contoller should be straightforward since we can stub/mock the service dependency and test the logic of the controller in isolation. Most of the tradeoffs revolve around how much state we want the service object to have as well as the type of response we get from the different service actions.

When I’m faced with potential tradeoffs I usually try to choose the most functional approach - even though Ruby is not a functional language you can apply functional concepts to your code in order to make it more readable, testable and maintainable. That means I tend to prefer less state and pure functions. While it’s not really possible to implement our service object using a pure function, we can at least minimize the amount of state maintained within the service object itself.

class UserService
  def create_user(user_attributes)
    user = User.new(user_attributes)
    if user.save
      send_welcome_email(user)
      Result.new(user, true)
    else
      Result.new(user, false)
    end
  end

  private

  def send_welcome_email(user)
    UserMailer.delay.welcome(user)
  end

  class Result
    attr_reader :user

    def initialize(user:, success:)
      @user = user
      @success = success
    end

    def success?
      @success
    end
  end
  private_constant :Result
end

And here is the consuming code for the service.

class UsersController < ApplicationController
  def create
    service = UserService.new
    result = service.create_user(create_user_params)
    if result.success?
      redirect_to root_path
    else
      @user = result.user
      render :new
    end
  end
end

Let’s consider the different responsibilities of the service object - specifically what it’s not doing.

1. It doesn’t maintain any state

We might change this if the service needs to access the current user or some other dependency - in which case we probably want to pass it into the constructor - but for right now there is no state - which is great. This makes the class easy to maintain and the actions easy to reason about.

2. It doesn’t do any validation

This responsibility lies with the model, not the service. We’re also not dealing with any error messages or doing any munging of form fields. This is a very simple example, but if we needed to manipulate the request parameters at all I would suggest we create a separate object that deals with this - we don’t want to add additional responsibilities to the service object.

3. The result of the service action is an actual Result object, not a boolean or User object.

This makes the action particularly easy to test - we simply need to call the action and inspect the result. (Although we still might want to test that the mailer is being invoked correctly.)

In the end we end up with a pretty simple service object that is responsible for coordinating the different pieces of logic associated with a specific use case. The controller now has very clear responsibilities - whitelisting params, security (authentication and authorization), rendering views (or redirecting) and invoking the appropriate service action. All of these responsibilities definitely belong in the controller.

Naming Your Services

There are only two hard things in Computer Science: cache invalidation and naming things.

– Phil Karlton

Earlier I mentioned that there is healthy debate about the best convention for naming services. A common starting point is to use name of the domain model you’re interacting with - UserService or ProductService. The downside to this approach is that it doesn’t provide a good name for actions which involve multiple domain models and implies that all actions associated with a certain domain model is done through a single service. Trying to group all actions for a given domain object into a single service will eventually lead to bloated services.

class UserService
  def create_user(...)
    ...
  end

  def update_user(...)
    ...
  end

  def delete_user(...)
    ...
  end
end

Another approach is to name your service according to the use case you’re supporting, so RegisterUser, AddProductToCart or LikeComment. This approach suggests that each service has a single responsibility and purpose. You can take this one step further and have a standard name for the action the service performs - this method is often called perform or call. An added benefit of using call as the name of your action is that the service can be stubbed with a lambda - since you invoke a lambda with call - helping to standardize controller tests.

class RegisterUser
  def call(...)
    ...
  end
end

Since we’re trying to adhere to SOLID design principles it makes sense for each service to have a single purpose. We therefore need to choose between RegisterUser and RegisterUserService. When I’m confronted with this type of naming decision, I prefer to consider which name would be preferred by someone new to the codebase and the decision becomes obvious. Even though explicit naming can be cumbersome it helps new engineers to navigate the codebase.

In terms of a standard name for the service action - I think an argument can be made for any approach, but being consistent is the most important. I have a slight preference for using call since it helps to standardize the tests as well as the services.

So to return to the first example, here is the service we end up with:

class RegisterUserService
  def call(user_attributes)
    user = User.new(user_attributes)
    if user.save
      send_welcome_email(user)
      Result.new(user, true)
    else
      Result.new(user, false)
    end
  end

  private

  def send_welcome_email(user)
    UserMailer.delay.welcome(user)
  end

  class Result
    attr_reader :user

    def initialize(user:, success:)
      @user = user
      @success = success
    end

    def success?
      @success
    end
  end
  private_constant :Result
end

And here is the controller consuming the service:

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    service = RegisterUserService.new
    result = service.call(create_user_params)
    if result.success?
      redirect_to root_path
    else
      @user = result.user
      render :new
    end
  end

  private

  def create_user_params
    params.require(:user).permit(:name, :email)
  end
end

Regardless of the ultimate semantics of your service objects, extracting your business logic into services will help keep your models and controllers ‘skinny’ - which leads to a better Rails application. Happy coding.