Using Service Objects in Ruby on Rails

When an application reaches a certain scale, architectural concerns start to surface. Basic guidelines for clean code are present in Rails and follow the Model View Controller structure:

  • No fat models; prevent them from becoming bloated.
    Keep views simplistic; don't include any logic.
    Don't overload the controllers with weight; keep them lean.

It also begs the fundamental question, "Where do I put all that code?"

Let's talk about service objects.

Ruby service objects can take the shape of a class or module that executes an operation and helps remove logic from other parts of the MVC framework. As an easy illustration, consider the following controller:

class PostsController < ApplicationController
  def create
    @title = params[:title]
    @content = params[:content]
    @post = Post.new(title: @title, content: @content)
    if @post.save
      flash.notice = 'Post saved'
      render @post
    else
      flash.alert = flash_error_message(@post)
      redirect_to :new
    end
  end
end

Once you comprehend the design pattern, extracting part of this into a service object is straightforward.

  • create a services folder in the Rails' app folder
  • create the service object file, in this example create_post.rb
  • extract the functionality to the CreatePost class/module
  • reload the Rails app and try it

Service objects as modules

I made a service that closely resembles a factory design pattern using a module approach:

module CreatePost
  class << self
    def execute(params)
      title = params[:title]
      content = params[:content]
      post = Post.new(title: title, content: content)
    end
  end
end

Consequently, the controller became much easier to control:

class PostsController < ApplicationController
  def create
    @post = CreatePost.execute(params)
    if @post.save
      flash.notice = 'Post saved'
      render @post
    else
      flash.alert = flash_error_message(@post)
      redirect_to :new
    end
  end
end

Service objects as classes

We utilize classes when we need to hold instance variables and other methods. Our code might be changed as follows using a class:

class CreatePost
  def initialize(params)
    @title = params[:title]
    @content = params[:content]
  end

  def call
    Post.new(title: @title, content: @content)
  end
end

The controller's code would be:

class PostsController < ApplicationController
  def create
    @post = CreatePost.new(params).call
    if @post.save
      flash.notice = 'Post saved'
      render @post
    else
      flash.alert = flash_error_message(@post)
      redirect_to :new
    end
  end
end

Organizing service objects with modules

Our "services" folder tends to expand significantly as we start using services. By employing folders and modules to build a modular framework, we can control this growth.

The variety of service objects and their many purposes in our program might be reflected in the "services" subdirectory. We utilize Ruby modules to namespace-group them.

module Post
  module Build
    def self.call(params)
      title = params[:title]
      content = params[:content]
      Post.new(title: title, content: content)
    end
  end
end

To enable Rails to load them, we must put them in folders that correspond to our module structure.

services/post/build.rb
services/post/update.rb
services/comments/build.rb
...

By doing so, we may scale our use of service objects to match the expansion of our program.