Rob Race

@rob__race

Service Objects in Ruby on Rails…and you

Where do I put this stuff?

Note: This tutorial is an excerpt for the service object chapter in my book Building a SaaS Ruby on Rails 5. The book will guide you from humble beginnings by deploying an app to production. If you find this type of content valuable, the book is on sale right now!

Also, the beta for my new project Pull Manager is almost ready. If you’re losing track of pull requests, have old ones lingering around or would just love a dashboard that aggregates these requests over multiple services (Github, Gitlab, and Bitbucket), check it out.

We have all been there when you see your controller action getting way too long and hold too much business logic. You know you need to email the user, adjust an account, maybe submit to Stripe and finally ping a Slack Webhook. Well, where should it go? This code needs to exist and doesn’t seem to fit in the model. This my friend, is where Service Objects come in!

What’s so great about a Service Object? I find in most projects there is nothing easier to reason about or test than a POO(Plain Old Object), which in our case is a PORO(Plain Old Ruby Object). Meaning, the Service Object will be a stand alone class the is not inheriting or extending any other classes from Ruby or Rails.

Note: I follow-up with this post, refactoring

Let’s begin!

Rails does not create a services folder by default so it is our job to create one. A little side-note though, depending on the size of your app or the complexity of business logic you may have the need to break services down even further by domain, type or other qualifiers. For the purpose of this post, you can just create a services folder in your app folder mkdir app/services

Restart your rails server to pick up the new folder as Rails will autoload directories in the app directory. Now we can create a file for a user registration service new_registration_service.rb and fill it with our business logic.

class NewRegistrationService
def initialize(params)
@user = params[:user]
@organization = params[:organization]
end
def perform
organization_create
send_welcome_email
notify_slack
end
private
def organization_create
post_organization_setup if @organization.save
end
def post_organization_setup
@user.organization_id = @organization.id
@user.save
@user.add_role :admin, @organization
end
def send_welcome_email
WelcomeEmailMailer.welcome_email(@user).deliver_later
end
def notify_slack
notifier = Slack::Notifier.new "https://hooks.slack.com/services/89ypfhuiwquhfwfwef908wefoij"
notifier.ping "A New User has appeared! #{@organization.name} - #{@user.name} || ENV: #{Rails.env}"
end
end

Ok! Lets go over the Service Object section by section!

class NewRegistrationService
def initialize(params)
@user = params[:user]
@organization = params[:organization]
end

Here we are creating the class and then adding the initialize method to create the needed instance variables upon the object being instantiated. In this case, we will pass the object the User model record object and the Organization record object previously created in a new user signup controller.

def perform
organization_create
send_welcome_email
notify_slack
end

Personally, I like to wrap the functionality of the whole set of business logic in a perform method. I have also seen some proponents of calling different methods manually from the controller as well. It’s really a matter of your preference.

def organization_create
post_organization_setup if @organization.save
end
def post_organization_setup
@user.organization_id = @organization.id
@user.save
@user.add_role :admin, @organization
end

Here we are creating the organization(which was previously validated in the controller that instantiated this Service Object) and then making the necessary updates to the user such as adding the organization_id needed for the belongs_to relation and assigning a role for the authorization system that is scoped to the previously created organization.

def send_welcome_email
WelcomeEmailMailer.welcome_email(@user).deliver_later
end

A quick call to a Rail’s ActionMailer with the user object…

def notify_slack
notifier = Slack::Notifier.new "https://hooks.slack.com/services/89ypfhuiwquhfwfwef908wefoij"
notifier.ping "A New User has appeared! #{@organization.name} - #{@user.name} || ENV: #{Rails.env}"
end

…and finally notifying the Slack Webhook I use for sign up notifications.

Now, you may be saying to yourself, this makes sense but how and where do you call this Service Object? The following is the code extracted from my Devise Registration controller handling the incoming sign up form and all it’s Devise goodness:

NewRegistrationService.new({user: resource, organization: @org}).perform

Some readers may start to question some of the aspects of the Service Object in this post. Service Objects do not have a strong convention that I have seen and I have fit this one to match my needs. Earlier, when I said you can call public methods individually, instead of using a perform method, would allow you to wrap some error handling logic. Such as this:

class NewRegistrationService
def initialize(params)
@user = params[:user]
@organization = params[:organization]
end
def organization_create
begin
post_organization_setup if @organization.save
rescue
false
end
end
....

..and in the controller, something like:

if NewRegistrationService.new({user: resource, organization: @org}).organization_create
**success logic**
else
** redirect_to last_path, notice: 'Error saving record'
end

Regardless of your Service Object structure, simply extracting your business logic out of your controllers and into Service Objects will help keep your controllers ‘skinny’ and your application’s code easy to follow.

Note: I follow up with this post with a refactored version of this Service

More by Rob Race

Topics of interest

More Related Stories