codete Rails Service Objects Complete Overview 1 main 7adb278bd9
Codete Blog

Rails Service Objects - Complete Overview

Maciej Ochalek cdc1338a01

11/03/2022 |

9 min read

Maciej Ochałek

The story about Rails Service Objects needs to start by saying a few words about Ruby on Rails per se. It is probably the best framework if you’re looking to create an MVP for your web application, known for its conciseness and great automation of the most common tasks. You can generate a fully functional CRUD with sensible defaults with just a few commands and easily customize it to suit your needs. And it doesn’t stop there.

While the most easy-to-start-with web technologies quickly become cumbersome and useless as your product grows more complex, Rails can still be used even in large, complex applications. 

But even for Ruby on Rails, code complexity means additional challenges: the same conventions and patterns, which make Rails apps clean and elegant (MVC; Fat Model, Thin Controller), make it hard to find a perfect location for the extended logic that grows under the hood of your application. The framework had no answer to this problem, which resulted in larger projects being inconsistent, not to say messy. Models have become god objects and even the introduction of Concern didn’t improve the situation a lot because it is yet another concept that breaks the “composition over inheritance” principle, making it a double-edged weapon.

 

Table of contents:

  1. Service Objects in Ruby on Rails in a nutshell
  2. Designing clean code with Ruby Service Objects
  3. Isn't the Service Object just a PORO?
  4. Should I always use Service Objects Rails?
  5. Where should I place Service Rails?
  6. Dessert: Some syntactic sugar
  7. Rails Service Objects boiled down

Service Objects in Ruby on Rails in a nutshell

Fortunately, finally, a (still unofficial) convention has emerged and it’s been called Service Objects. And before I explain what a service object is, here’s a little disclaimer: I hate the name. When I hear the word “service”, my brain suggests an independent long-running task, probably one utilizing its own process, as it happens in Android or Windows, a synonym of “daemon”. The definition of Ruby on Rails service objects couldn’t be further away from that.

A Service Object in Rails is just a way of encapsulating a part of logic, a single action or function, into an independent single-use object of a non-anonymous class. In other words, it’s a closure extended into a PORO, with all its benefits, including the ability to properly initialize it with arguments and access its state at any time. However, the Service Object still serves a single purpose and exposes only one public method, and basically is built around that method. 

Designing clean code with Ruby Service Objects

Time for a potentially real-life example. Everybody knows the game “Wordle”, right? Let’s make its clone. 

As a backend developer we’re supposed to implement an API action that evaluates the guessed word by comparing it letter by letter with the word of the day and sending a feedback response comprising an array of one of the three possible results for each of five letters:

include ResultConstants
def guess
  head 400 and return unless params[:guess].present? && params[:guess].length == 5

  feedback = params[:guess].split('').map.with_index do |letter, index|
    if letter == word_of_the_day[index]
      CORRECT_LETTER_IN_CORRECT_POSITION
    elsif word_of_the_day.include? letter
      CORRECT_LETTER_IN_WRONG_POSITION
    else
      WRONG_LETTER
    end
  end

  render json: { feedback: feedback }
end

private

def word_of_the_day
  @word_of_the_day ||= WordOfTheDay.find_by(date: Date.current)
end

We could extract the evaluation of the guess (the business logic) into another controller method, but it wouldn’t make the controller shorter or simpler. And it’s totally not what the controller is supposed to do.

We could move it into the WordOfTheDay model, but wouldn’t it make it too complex? Wouldn’t it violate the Single Responsibility principle? After all, the model is just the abstraction of the database record and not a container for the business logic. What if the same WordOfTheDay was reused in a different word game, say, “Hangman”?

We could also make it a concern, but a concern becomes a part of the controller or model, so the change is only in the form, not semantics. 

Enter Service Object. Let’s create one and move business logic into it:

class GuessEvaluator
  include ResultConstants
  attr_reader :date

  def initialize(date)
    @date = date
  end

  def call
    params[:guess].split('').map.with_index do |letter, index|
      if letter == word_of_the_day[index]
        CORRECT_LETTER_IN_CORRECT_POSITION
      elsif word_of_the_day.include? letter
        CORRECT_LETTER_IN_WRONG_POSITION
      else
        WRONG
      end
    end
  end

  private

  def word_of_the_day
    @word_of_the_day ||= WordOfTheDay.find_by(date: date)
  end
end

The Service Object encapsulates the whole business logic of evaluating a guess, and is even independent of date: it can be used to guess words that the player has missed in the past. 

The main parts of the Service Object are: the thin initializer and the public method that encapsulates the actual business logic. It may be named call, perform or execute,  there’s no single convention for that. What’s more important is that it’s just the only method exposed by the object.

Let’s modify the controller action to use the Service Object:

def guess
  head 400 and return unless params[:guess].present? && params[:guess].length == 5

  feedback = GuessEvaluator.new(Date.current).call
  render json: { feedback: feedback }
end

Now the controller is only responsible for routing, validation of parameters, and rendering of the response - the Rails stuff. Just as it should be. The business logic is safely kept in the Service Object.

Isn’t the Service Object just a PORO?

Every Service Object is a Plain Old Ruby Object (PORO), but is it “just a PORO”? Well, not exactly.

Even though a formal definition of Service Object does not exist, a few things are common among its variants:

  • Service Object is single-responsibility and single-use;
  • As for the call method, it only exposes a single public method;
  • It may perform complex operations but does not require complex initialization or dependencies. It just consumes params, performs its action, and returns a value.

The returned value should clearly indicate if the Service Object’s work succeeded. It can be a true/false value, a status Enum, or something produced by the call (a generated report or just the evaluation of somebody’s Wordle guess). One must not forget that SO is built around the method it exposes and is basically a quasi-functional concept.

The above features not only make the Service Object a perfect building block to help break the monolith of your code into but, what’s probably even more important, help minimize dependencies and greatly improve the portability and testability of the critical parts of your code. 

Should I always use Service Objects Rails?

Service Object is a perfect way to encapsulate a specific piece of business logic. It helps decouple it, wraps it with a simple API, and, to some extent, even controls its lifecycle. 

A Service Object probably shouldn’t be used when it comes to Rails-specific concepts, like URLs, params, or access control. Making use of it may also not be the best way to share code among controllers or models - concerns provide better integration with the “Rails stuff”. And integration is the opposite of what Service Objects are for.

Where should I place Service Rails?

Since the makers of Ruby on Rails still don’t recognize Rails Services as full-fledged components of the Rails app, there’s also no specific location where they should be placed. However, as the pattern gained popularity, there’s been a growing tendency to store Service Objects in the /app folder, rather than /lib. Make it /app/services or /app/poros, it’s up to you. But it’s been harder and harder to imagine a complex app without Service Objects, and SO’s been the /app directory.

Dessert: Some syntactic sugar

As we’re getting close to wrapping the topic of Service Objects up, let’s reiterate the second sentence of this article: Ruby is known for its conciseness. But is Service Object a part of it? Isn’t it from the inhospitable land of bloated code ruled by the Gang of Four? 

Service Object may come from that place, but it’s been raised in the Ruby world. Thus, it believes in maximum productivity per line of code and the joy of coding. So let’s make the Ruby Service Object sweeter (but without any additional calories!). 

We’ve been calling the SO’s obviously named public method like this:

  feedback = GuessEvaluator.new(Date.current).call

We call two methods in a chain: new with arguments and call without. Do we need both? Nope. 

We can handle it with a simple concern: 

module ServiceObject
  extend ActiveSupport::Concern
  
  def self.call(*args, &block)
    new(*args, &block).call
  end


  included do
    private_class_method :new
  end
end

Notice that it implements a class function call, that initializes the object before calling its instance method call. To prevent temptation, it also reduces the visibility of the new class method to private, making self.call the only way to instantiate and execute the service object.

Thus, we end up with a class that only exposes one class method, without all the disadvantages of class methods. Testable and with zero boilerplate. You call it like this:

feedback = GuessEvaluator.call(Date.current)

Rails Service Objects boiled down

Heading towards the end of this story, we may ask the vital question: Is Service Object the beginning of the end of Ruby on Rails as we know it? Well, it’s good to know that some developers, including David Heinemeier Hansson, are in strong opposition to Service Object. They argue that Rails comes with equipollent alternatives. They hate the idea of the conjunction of the spheres of the dull and bloated enterprise programming with Java, C# and their evangelists, and the joyful, yet still somehow immature Ruby on Rails.

The truth is, however, if Rails is to stay above the surface in the third decade of the 2000s, it must evolve as an enterprise framework, allowing more and more complicated business logic to be accommodated in apps without losing the joy of coding, the biggest advantage of Ruby on Rails. It seems that Service Objects might be one of the ways to achieve this goal.

And you, how do you find the effectiveness and usability of the Rails Service Objects? In which cases are Ruby on Rails Services the most useful? Is separating a business action into its own service object vital? What do you think the future will hold for service objects?

Rated: 5.0 / 3 opinions
Maciej Ochalek cdc1338a01

Maciej Ochałek

Experienced developer, architect and tech lead, acclaimed scrum master and coach. Most happy to work with Ruby on Rails and *nix, but always striving and able to see the big picture. By some, dubbed the “All Things Specialist”.

Our mission is to accelerate your growth through technology

Contact us

Codete Global
Spółka z ograniczoną odpowiedzialnością

Na Zjeździe 11
30-527 Kraków

NIP (VAT-ID): PL6762460401
REGON: 122745429
KRS: 0000983688

Get in Touch
  • icon facebook
  • icon linkedin
  • icon instagram
  • icon youtube
Offices
  • Kraków

    Na Zjeździe 11
    30-527 Kraków
    Poland

  • Lublin

    Wojciechowska 7E
    20-704 Lublin
    Poland

  • Berlin

    Bouchéstraße 12
    12435 Berlin
    Germany