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
- 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 User
model.
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.
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! 🙏