Rails Best Practice: Services

A service is an independent module which contains logic that doesn’t belong to one model or controller. As the name indicates, it provides a service to a model, controller or even another service. It does one thing and does it well. The determination of creating or use a service based on a specific thing that the logic does (Single Responsibility)

Think of this example of a property owner who hires a property manager to manage this property. The property manager provides a management service. This manager in turns hires a plumbing contractor, a carpet cleaner, … to help him fix the property’s issues. These contractors including the manager provide a service that they do well: management, plumbing fixes and carpet cleaning service. At the end, the property owner only needs to know the manager and gets the report from him; s/he doesn’t need to deal with the other contractors or to know about their existence. That will keep things organized and simple to maintain. #lessstress

What should be in a Service?

The logic that:

  • is complex (such as calculating an employee’s salary)
  • uses APIs of external services
  • clearly doesn’t belong to one model (for example, deleting outdated data)
  • uses several models (for example, importing data from one file to several models)

Naming convention

Ideally, a service should perform only one action and should be named after this behavior or role. For example:

  • ImageService
  • PurgeScheduler

Signs

  1. When libraries are included in the controller for a specific service

For example, we see the Magik image upload libraries are included inside the UsersController. We know that when a library is included in the controller, the logic that uses this library is also in the same controller. In this case, the logic of uploading an image does not belong to the UsersController alone nor the Usermodel.

class Api::UsersController < ApplicationController
require ‘RMagick’
include Magick
... logic
end

To refactor this, I would create a UploaderService or in this case, I created a namespace ImageService since I have in mind what else I want to include here to handle images: processing, uploading, etc…Hence,ImageService::Uploader

class ImageService::Uploader
attr_reader :path, :filename
require ‘RMagick’
include Magick
... logic
end

2. When logic does not belong to a controller, but not necessarily belongs to a controller or the associated model. (example: the uploader logic inside a UsersController)

In the example below, you can see inside the upload_photo inside UsersController , there are code in which the logic doesn’t belong to the controller, but in the services or model. Looking into the logic further, we can see what one of the logics does is to prepare the image for upload, another uploads images to S3, another saves the image info into the database in the User model. We will relatively move these logic into a uploader service and into the initializer(prep images for upload), into a function called upload_to_s3, and the database related code in the User model in a function called save_photo_info

Before:

# inside UsersController
def upload_photo
return head :bad_request unless params[:file]

# This code belongs to a Service
filename = params[:file].try(:original_filename) || params[:file][:original_filename]
path = params[:file].try(:path) || params[:file][:path]
path = path.gsub(‘https://’, ‘http://')
image = ImageList.new(path)
image.auto_orient!

begin
# This code belongs to a Service s3_filename = “user_photos/#{DateTime.now.utc.strftime(“%Y%m%dT%H%MZ”)}_#{SecureRandom.hex}_#{filename}”
Utils.upload_content_to_s3_bucket(image.to_blob, ENV[“UPLOADS_BUCKET”], s3_filename)
url = “https://s3.amazonaws.com/#{ENV["UPLOADS_BUCKET"]}/#{s3_filename}"
# This code belongs to User model @user.photo = url
@user.needs_profile_pic = false if params[:source] && params[:source] == ‘vendor_mobile’
@user.save!

After:

def upload_photo
return head :bad_request unless params[:file]

begin
photo_url = uploader.upload_to_s3 # also returns the uploaded photo’s url
user.save_photo_info(photo_url, params[:source])
private
def uploader
@uploader ||= ImageService::Uploader.new(params[:file][:path], params[:file][:original_filename])
end

One common mistake that I see new developers make often is that the model related code is put in the controllers. One thing to keep in mind when writing controller code is to keep logic that deals with model logic / database inside the models as much as possible instead of in the controller. If you are conscious of writing clean code while writing code (as opposed to make it work now and clean it later), you won’t have as much technical debt accumulated and save much time debugging or maintenance later on.

Let’s keep your code organized, folks!

If you enjoyed this article, please recommend it by hitting the clap icon that you’ll find at the bottom of this page so that more people can see it on Medium. Thanks! 🙏