ryanwhocodes Jan 27, 2018 · 6 min read

A Beginner's Guide to Service Objects for Ruby on Rails

How you can create Service Objects for Ruby on Rails apps and use them to refactor Models, Views and Controllers.

Rails follows a Model-View-Controller pattern. This raises questions around where programming logic should go once a Ruby on Rails application reaches a certain size. Generally, the principles are:

  • Forget fat Models (don’t allow them to become bloated)
  • Keep Views dumb (so don’t put complex logic there)
  • And Controllers skinny (so don’t put too much there)

So where should you put anything that’s more than a simple query or action? - Consider using Service Objects.

Contents

Introducing… The Service Object

One way to support Object-Oriented Design is with the Service Object. This can be a class or module in Ruby that performs an action. It can help take out logic from other areas of the MVC files. Trying this out for the first time in a Rails project, I started with a bloated controller that included the following method:

class SuggestionController < ApplicationController
  def create
    @topic_name = params[:topic_name]
    @suggestion_text = params[:suggestion_text]
    @suggestion = Suggestion.new(topic: Topic.first(name: @topic_name, text: @suggestion_text))
    if @suggestion.save
      flash.notice = 'Suggestion added!'
      render @suggestion
    else
      flash.alert = create_fail_error_message(@suggestion)
      redirect_to :new
    end
  end
end

Extracting this to a Service Object is relatively painless once you understand the design principles.

  • Create a services folder within the Rails’ app folder
  • Create your Service Object file, for example suggestion_service.rb
  • Extract the code from the controller into the SuggestionService class/module
  • Reload your Rails app and try it out!

Create Service Objects as Modules

I decided to go for the module approach, and used a build method to create a new service. This made it look very similar to a factory design pattern.

module SuggestionService
  class << self
    def create(params)
      topic_name = params[:topic_name]
      suggestion_text = params[:suggestion_text]
      topic = Topic.find_by(name: topic_name)
      Suggestion.new(topic: topic, text: suggestion_text)
    end
  end
end

It also made the controller’s code a lot more manageable.

class SuggestionController < ApplicationController
  def create
    @suggestion = SuggestionService.create(params)
    if @suggestion.save
      flash.notice = 'Suggestion added!'
      render @suggestion
    else
      flash.alert = create_fail_error_message(@suggestion)
      redirect_to :new
    end
  end
end

To use extraction and refactoring like this in production level applications, you need to include some way of handling errors gracefully when creating the Service. CreateFailErrorMessageService Service Object, anyone?

def create_fail_error_message(record)
  "Could not create #{record.class}" \
  " because #{record.errors.full_messages.join(', ')}"
end

This is currently in my ApplicationController (which my other controllers inherit from), but could easily be extracted to other Service Objects.

Create Service Objects as Classes

Other flavours of Service Objects use classes instead of modules that might store some instance variables and have other methods to support the service. My code could be rewritten as:

class NewSuggestionService
  def initialize(params)
    @topic_name = params[:topic_name]
    @suggestion_text = params[:suggestion_text]
  end

  def call
    topic = Topic.find_by(name: @topic_name)
    Suggestion.new(topic: topic, text: @suggestion_text)
  end
end

And the code would then look slightly different in the controller.

class SuggestionController < ApplicationController
  def create
    @suggestion = NewSuggestionService.new(params).call
    if @suggestion.save
      flash.notice = 'Suggestion added!'
      render :show
    else
      flash.alert = create_fail_error_message(@suggestion)
      redirect_to :new
    end
  end
end

If you start using more and more Service Objects you might find you end up with a rapidly-expanding services folder. You can manage this growth by organising them into folders and modules.

Manage Many Service Objects with Modules

Once you understand the principles of Service Objects, it is easy to build upon this. And the service folder structure can be expanded to reflect the different varieties of objects and purposes it serves. As you create more and more Service Objects, they can be grouped into namespaces to organise them using modules.

module Suggestion
  module Build
    def self.call(params)
      topic_name = params[:topic_name]
      suggestion_text = params[:suggestion_text]
      topic = Topic.find_by(name: topic_name)
      Suggestion.new(topic: topic, text: suggestion_text)
    end
  end
end

Then place them into a folder path to reflect the module structure otherwise Rails won’t load it automatically. You may want to simplify the naming at this point.

services/suggestion/build.rb
services/suggestion/destroy.rb
services/topic/build.rb
...

This can make the use of Service Objects scalable as your app grows in size.

Think about what you want each Service Object to achieve, then create other namespaces for more groups of related Service Objects with clearly defined responsibilities and purposes. Folder categories could be related to each model, process, or type of service, such as Search.

services/suggestion/builder.rb
services/login/validator.rb
services/search/suggestions_by_tag.rb
...

Now that you’ve identified the responsibility for each Service Object, next I will show you how to extract code into them from other parts of your Rails app.

How You Can Refactor Models, Views and Controllers with Service Objects

When you first start a small Ruby on Rails app, it may not seem necessary to modularise your code and create lots of extra Service Objects. But as your app grows, it can also lead to complex and long code blocks in Models, Views and Controllers that can be tricky to change later.

To ensure that extracting code to a new Service Object from other areas of your app does not break existing functionality, it is a software development best practice to do this in a test-driven way.

Hopefully, you will have existing unit tests for your code that you can move into a test file for your new Service Object. If you don’t, you need to ensure that all areas of the new class or module are tested. It is also important to have integration tests to see how the change will impact other areas of your app when your Service Object is called.

This will demonstrate a code extraction from a Controller to a Service Object, but the same principle can apply to other parts of a Ruby on Rails app. Here is an example of how to do this in three steps.

Step 1: Identify code to extract from a controller.

class SuggestionController < ApplicationController
  def create
    topic_name = params[:topic_name]
    suggestion_text = params[:suggestion_text]
    topic = Topic.find_by(name: topic_name)
    @suggestion =
      Suggestion.new(topic: topic, text: suggestion_text)

    if @suggestion.save!
      render(:show)
    else
      redirect_to(:root)
    end
  end
end

Step2. Extract code to a Service Object.

module Suggestion
  module Builder
    def self.call(params)
      topic_name = params[:topic_name]
      suggestion_text = params[:suggestion_text]
      topic = Topic.find_by(name: topic_name)
      Suggestion.new(topic: topic, text: suggestion_text)
    end
  end
end

Step 3. Refactor the controller to replace existing code with the Service Object.

class SuggestionController < ApplicationController
  def create
    @suggestion = Services::Suggestion::Builder.call(params)

    if @suggestion.save!
      render(:show)
    else
      redirect_to(:root)
    end
  end
end

Now you have the hang of creating a Service Object, you can use them to further clean up your code using guard clauses.

Guard Clauses

Guard clauses are a way of writing code that guards the flow of logic from continuing if certain conditions are met, or not met.

return ACTION if CONDITION

Service Objects can be used to design or refactor code so that logic is kept out of other parts of a Ruby on Rails app, such as Controllers.

In this example, you can see how responsibilities for managing the user, Suggestion record, and view rendering are handled outside of the Controller by Service Objects. This shows how they can be used for both ACTION and CONDITION purposes within guard clauses.

class SuggestionController < ApplicationController
  def create
    return redirect_to(:root) unless
      Services::User::Validator.call(params)

    @suggestion =
      Services::Suggestion::Builder.call(params)

    return Services::Suggestion::ViewRenderer.call if
      @suggestion.save!

    redirect_to(:root)
  end
end

Find Out More

This article has given you some ideas of how to use Service Objects with your Ruby on Rails app. But it is more of a guide than a rule, and you can adapt the concepts to the needs and design of your project.

The main aims are to follow the principles of clear responsibilities, modular design, and test-driven development to create a great app that is stable, maintainable, and extendable.