ActiveRecord Refactoring: Services

Continuing from my previous post, which introduced the idea of concerns to your models, in this post I will be discussing using services to keep your ActiveRecord’s responsibilities toned down.

In this short series I will be looking at three ways of reducing the responsibilities of your models:

Services, also known as Interactors, are a Rails pattern with the aim of keeping the logic in a controller clear and concise without passing off controller-like responsibility to the model. Another major reason for using this pattern is to increase the amount of code you can test in isolation without booting Rails as abhorred by DHH and taken to crazy lengths in Jim Weirich’s Decoupling from Rails talk.

So what do I mean by controller-like responsibility? Controllers are the glue between the rest of your application.

In the Rails world a controller is generally an interface for the CRUD operations of an ActiveRecord model. This involves handling the HTTP parameters of a request and using them to change data in an application, e.g. updating a user’s profile. When making such a change the controller must ensure the model was happy with the update made by checking for validity. It must then pass errors back to the user if any occurred, say if a required field was missing, so that the user can correct the problem. On successful updates a controller will redirect and set a success flash message.

Controllers are also used for sending mail using an ActionMailer class. Generally speaking this happens after another action of some kind, e.g. sending a welcome email after a user registers.

These are just some of the controller-like responsibilities I see in applications I work with. I’m sure there are many other things they are used for.

You may now be asking, what does this have to do with refactoring ActiveRecord models? Well in my experience you often see some of the responsibilities listed above leak their way into other parts of the application. Let’s look at two examples that illustrate this.

The first example looks at the need to create new Artists when creating a new Event for a gig listing site.

class Event < ActiveRecord::Base
  has_and_belongs_to_many :artists
  accepts_nested_attributes_for :artists
end

class Artist < ActiveRecord::Base
  has_and_belongs_to_many :events
end

Okay, so ActiveRecord handles most of the logic for the relationship, you’d likely be calling Event#create in your controller #create action:

class EventsController < ApplicationController
  def create
    @event = Event.create(params[:event])
    if @event.valid?
      flash[:success] = 'Saved'
      redirect_to events_path    
    else
      render :new
    end
  end
end

Now things get hairy if we want to email each artist to say they’ve been added to an event. I often see this placed in the model:

class Events < ActiveRecord::Base  
  has_and_belongs_to_many :events
  after_create :notify_artists  
  def notify_artists
    artists.each do |artist|
      EventMailer.notify_artist(self, artist).deliver_later
    end  
  end
end

Your controller can stay the same. This isn’t so bad right? But what if you then need to add a delayed job to poll for the artist’s SoundCloud details?

class Events < ActiveRecord::Base
  # ...  
  after_create :poll_soundcloud
  def poll_soundcloud
    artists.each do |artist|
      ArtistSoundcloudJob.perform_later(artist)
    end  
  end
end

Now the Events model has methods for sending welcome emails to artists and requesting jobs to be performed later. At first glance this might not seem too problematic. When you test the Events model however you will now need to stub or mock test the calls out to EventMailer and ArtistSoundcloudJob. The responsibilities grow as we add more emails and jobs. You might send the event creator an email, or grab the tweets of the artists too.

You might alternatively like to keep this logic out of your models data layer and instead call within your controller.

class EventsController < ApplicationController
  def create
    @event = Event.create(params[:event])
    if @event.valid?
      notify_artists
      poll_soundcloud
      flash[:success] = 'Saved'
      redirect_to events_path
    else
      render :new
    end
  end  

  def notify_artists
    @event.artists.each do |artist|
      EventMailer.notify_artist(@event, artist).deliver    
    end  
  end  

  def poll_soundcloud
    @event.artists.each do |artist|
      ArtistSoundcloudJob.perform_later(artist)
    end  
  end
end

The logic is now held by the controller. This can grow quite large too however. Think about when we add the email notification to the event creator, or grab the artist’s tweets. Not only that but we might add emails and jobs for the other CRUD operations, e.g. sending artists emails if the event is cancelled. These are all realistic possibilities.

Enter the Service class. Generally a service will handle one CRUD action. You might have classes such as EventCreate, EventUpdate and EventDelete for the Event model. Let’s move the #notify_artists and #poll_soundcloud logic into an EventCreate service.

class EventCreate
  def exec(attrs)
    event = Event.create(attrs)
    if event.valid?
      notify_artists
      poll_soundcloud    
    end    
    event  
  end  

  def notify_artists
    @event.artists.each do |artist|
      EventMailer.notify_artist(@event, artist).deliver
    end
  end  

  def poll_soundcloud
    @event.artists.each do |artist|
      ArtistSoundcloudJob.perform_later(artist)
    end
  end
end

The controller should be updated too.

class EventsController < ApplicationController
  def create
    @event = EventCreate.new.exec(params[:event])
    if @event.valid?
      flash[:success] = 'Saved'
      redirect_to events_path
    else
      render :new
    end
  end
end

The controller looks very much as it did at the start but it calls EventCreate#exec instead of Event.create. The difference lies in the fact that neither the controller nor the model now know about mail sending and job queuing, and we gain a number of advantages from this.

We can create services for each CRUD operation and thus keep class length down which means classes are easier to read, understand and maintain.

Since classes are smaller, the unit tests too can stay smaller. There is less mocking when testing controllers and models. In a controller you can stub or mock the EventCreate out completely. In the model you needn’t stub or mock anything. In the service itself you can stub or mock the model, mailer and job queuer.

So there you have it. An explanation on using services to keep logic out of ActiveRecord models and your controllers too. Next time I will be discussing the use of decorators and how you can keep your models and views clean with them!

About the Author

Luke Morton

Chief Technology Officer at Made Tech. Talk to me about delivering value through digital and technology, how inclusion and feminism can address a digital skills shortage, and on creating mentoring and coaching cultures.

Avatar for Luke Morton

We are hiring! Find out more about a career at Made Tech.

Download a copy of our new book

Legacy technology is one of the biggest threats to public sector organisations.
Whether you’ve started your journey already or don’t know where to begin, this 160-page book has been written to guide you to define and implement the right approach for your organisation.