Skip to content
PLAY VIDEO PLAY VIDEO PLAY VIDEO
By Sameera Gayan

Keeping your classes small and maintainable with Service Objects

Keeping your classes small and maintainable with Service Objects

If you are familiar with Rails, you know that it has a predefined directory structure. Rails was one of the early adaptors of the MVC (Model, View, Controller) pattern. In fact that is one of the key strengths of the framework; it is easy to learn since everything has its own place. This is all well and good if your Rails app is relatively a small one - but when your app starts growing with features and functionality, soon you will find some code snippets that don’t seem to fit into the standard Rails directory structure. This is when these methods tend to get pushed to the ActiveRecord models. However, not all of these methods directly relate to a model; often these methods contain some validations required by the business/client.

In such scenarios, implementing the logic via service objects or services would be a good idea. Simply put, a “service object” is a Ruby class that contains some of the application's business logic without pushing it to the ActiveRecord layer. Often, a Service is a PORO (Plain Old Ruby Object).

Mmm.. that’s not very helpful - Let's try to explain with an example

Let's say you are developing a ticket booking system in Rails. So your client has the following requirement:

"After a user purchases a ticket he/she should receive the ticket by email."

Then you'll say, “Okay sure, that's not a problem, let’s send an email after the ticket has been purchased”, and you will start with adding the following code into your controller:

(Please note that I’m assuming that all the controllers/models and methods are already implemented at this stage)

   # app/controllers/tickets_controller.rb #create

   if @ticket.save
     Email.send_email_to_user(current_user.email, @ticket)
     # code
   else
     # code
   end

So, you get it working and the client is happy, and all is well. But then a few days later the client adds another requirement:

"We should send the user not only an email, but also an SMS if we have their mobile number."

Ahh... now you have two main options for tackling this problem:

One option would be to add a condition in the tickets_controller itself and call the SMS sending function. But wait…no!!! That will make your controller fat, which is against the Rails philosophy of skinny controllers.

The second option would be to create a separate method and move the email and SMS sending into that.

So you create a method in the User model and move the code there.

  # app/models/user.rb
  class User < ActiveRecord::Base
    # code

    # this method is called from the controller
    def send_notifications(ticket)
      Email.send_email_to_user(self.email, ticket)
      Sms.send(self.mobile, ticket) if self.mobile
    end
  end

You’ve saved the day! Now, whenever the client needs to add a new notification channel, all you have to do is add it to the send_notifications method.

However, as these notifications and business logics are added over time, you will notice that:

1) Even though the notifications are sending to a user, it's not core functionality of the User class.

2) Adding more and more business logic inside the User model makes it too bulky, and will eventually make the code too hard to maintain.

3) Testing the model/method without stabbing/mocking the other models becomes too difficult.

Lets try to understand these points:

(1) If you look closely at the send_notifications method, sending a notification is actually a supporting function for the User entity, as opposed to a core one. Core functions would be for example, adding a new user or deleting an existing user. But where else can we implement this method? With the current Rails stack the User model seems to be the only logical place.

(2) When the client wants to add to the business logic, our User model ends up getting more and more functionality and soon becomes too big and hard to maintain. Think again about the following client requirement:

"We need to send the User an SMS if they have enabled the option send me the ticket link via SMS."

In your application’s business context, sending a notification after validating all these business rules should fall under the notification domain, not the user domain.

(3) When you writing unit tests for the User model, you now have to think about resulting method calls to these other classes, like Email. This becomes more obvious if you have used callbacks (like after_save), because you will have to stub the method of the Email class to test the User model.

Too much required data setup for a unit test starts to indicate a code smell. In this case it's because our User model is doing things not directly related to a user’s domain.

When you start running into these dilemmas, it’s usually an indication that you should rethink your code structure. In this particular case we can move the notification logic to a Service Object.

Ok... Let’s build a ‘service object’

Even though Rails comes with a default directory structure, nothing prevents you from adding new directories. So first let’s add a directory called services under app. This will be the container for our service classes - just like Rails has a Model directory for ActiveRecord models.

At this point, you can decide how you would like to organise your directory/file structure. Some may prefer to have subfolders for each service type and some may just add all the services under the services folder. It's really up to you.

Once you have created the folder let’s add our notification service - I’m going to call it ‘UserNotificationService’; so create a file called usernotificationservice.rb under the services folder.

Service Folder structure

Now let’s add our new folder to load path, so that Rails knows about it when the app starts:

# config/application.rb
module <Rails Application Name>
  class Application < Rails::Application
    # code
    config.autoload_paths << Rails.root.join('services')
    # code
  end
end

Now we need to write the code. As I mentioned before, services are normally POROs, it’s totally up to you to decide how to implement them.

The following is one way of doing it (please note that exception handling is not included for the sake of simplicity):

# Sending notifications to users after a ticket has been purchased
class UserNotificationService
  def initialize(user)
    @user = user
  end

  # send notifications
  def notify(ticket)
    Email.send_email_to_user(@user.email, ticket)
    Sms.send(@user.mobile, ticket) if @user.mobile
  end
end

As you can see, we can now add more and more business rules to our notification feature without polluting the User ActiveRecord model. This also gives you a clearer separation between your business logic and ORM layer (ie, ActiveRecord).

The service is now called like the following:

   # app/controllers/tickets_controller.rb #create

   if @ticket.save
     UserNotificationService.new(current_user).notify(@ticket)
     # code
   else
     # code
   end

“That’s all good” you might say, but how do you test them?

Testing services is not hard at all. I’m using RSpec for my example, but you can use any Rails testing framework that you prefer.

In testing the service object, we only test logic defined in the class itself, and method calls out to other classes; not how other classes implement their own logic. For example, we only test to see if the UserNotificationService calls send_email_to_user in the Email class. The actual implementation of send_email_to_user should be tested in in the spec/test of the Email class.

To do this, create a directory called services inside the spec directory. Then create a file called user_notification_service_spec.rb.

Service spec structure

We can now spec out the logic of the service object like so:

require 'rails_helper'

describe UserNotificationService do
  let(:user) { FactoryGirl.create(:user, mobile: mobile) }
  let(:ticket) { FactoryGirl.create(:ticket) }

  subject(:notification) do
    UserNotificationService.new(user).notify(ticket)
  end

  context 'when the user does not have a mobile number' do
    let(:mobile) { nil }

    it 'send an email' do
      expect(Email).to receive(:send_email_to_user)
      notification
    end

    it 'does not send an SMS' do
      expect(Sms).to_not receive(:send)
      notification
    end
  end
end

When to use ‘service objects’

So, when exactly do you use service objects? The truth is that there is no hard and fast rule. Normally the services are better for mid to large systems; those with a decent amount of logic beyond the standard CRUD to create, update, delete and view resources. So whenever you think that a code snippet might not belong to the directory where you were going to add it, it’s probably a good idea to re-think and see if it should go to a service instead.

The conclusion...

Phew… Implementing a service is not too hard, right? Service objects are a great way to organise your code and business logic. They will make your code more readable, maintainable and testable. So basically, from now on, try to use services objects whenever applicable, remembering to keep your services clean and manageable.

Happy coding!

Latest Articles by Our Team

Our expert team of designers and developers love what the do and enjoy sharing their knowledge with the world.

We Hire Only the Best

reinteractive is Australia’s largest dedicated Ruby on Rails development company. We don’t cut corners and we know what we are doing.

We are an organisation made up of amazing individuals and we take pride in our team. We are 100% remote work enabling us to choose the best talent no matter which part of the country they live in. reinteractive is dedicated to making it a great place for any developer to work.

Free Community Workshops

We created the Ruby on Rails InstallFest and Ruby on Rails Development Hub to help introduce new people to software development and to help existing developers hone their skills. These workshops provide invaluable mentorship to train developers, addressing key skills shortages in the industry. Software development is a great career choice for all ages and these events help you get started and skilled up.

  • Webinars

    Webinars

    Webinars are our online portal for tips, tricks and lessons learned in everything we do. Make the most of this free resource to help you become a better developer.

    Learn more about webinars

  • Installfest

    Installfest

    The Ruby on Rails Installfest includes a full setup of your development environment and step-by-step instructions on how to build your first app hosted on Heroku. Over 1,800 attendees to date and counting.

    Learn more about Installfest

  • Development Hub

    Development Hub

    The Ruby on Rails Development Hub is a monthly event where you will get the chance to spend time with our team and others in the community to improve and hone your Ruby on Rails skills.

    Learn more about Development Hub

Get the “reinteractive Review” Monthly Email