Refactoring Rails Application according to SRP with Interactors

Software development practices change with time and the practices that were used before are being continuously replaced by some new practices that we call “Best Practices”. In this article, we will discuss something similar that is related to refactoring the code in a Rails application following SRP (Single Responsibility Principle).

In this blog, I am taking the liberty of assuming that you know how to code in Ruby and you also have knowledge of Rails and a few of the methods that some gems provide like current_user by devise gem, how mailer works and what are callbacks. So at some points, I may miss the internal details just to explain the actual point. I would not be writing much logic in there, just a comparison of old school practices and some new practices to structure the code.

If you are a Rails developer, you must have heard things like thin controllers, putting the logic in concerns, making use of service object pattern to clean up the controllers and all that. All the good rails applications follow such techniques to clean up the code and making controllers as this as possible. Well, talking about modern rails applications, we have a pattern of interactors that does cleaning and refactoring for us keeping in consideration SRP.

Now assume a use case of a company that registers a user and after successful registration, it gives him a free trial of a subscription and also sends a welcome email.

Following old school practices, we would do something like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RegistrationsController < ApplicationController
  def create
    user = User.new(user_params)
    if user.save!
        flash[:success] = "You have successfully registered."
        redirect_to dashboard_path
    else
      flash[:error] = "There was a problem during registration."
      render :new
    end
  end

  private

  def user_params
    params.require(:session).permit(:email, :password)
  end
end

Your user model would do something like this.

1
2
3
4
5
6
7
8
9
10
11
class User < ApplicationRecord
    after_create :register_free_trial, :send_welcome_email

    def register_free_trial
        subscription.create(.....)
    end

    def send_welcome_email
        WelcomeMailer.send(self) if subscription.present? && subscription.free_trial == true
    end
end

This code needs to be refactored and you might think what is the need for refactoring this code when it already looks so clean? Well, two reasons for that, which I will explain later in this article.

Let’s refactor first!.

For this, we will use interactor gem. In your Gemfile, put

1
gem interactor

Run bundle install.

Inside app/interactors create a file called register_user.rb and put the following code there.

1
2
3
4
5
class RegisterUser
  include Interactor::Organizer

  organize CreateUser, StartFreeTrial, SendWelcome
end

We could do all these operations in a single interactor but that would violate SRP, so we would be making use of an organizer that automatically calls the next interactor if success is returned from the previous one.

The next interactor would be create_user.rb in the same directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CreateUser
  include Interactor

  def call
    user = User.create(context.user_params)

    if user.persisted?
      context.user = user
    else
      context.fail!
    end
  end

  def rollback
    context.user.destroy
  end
end

Then we also need to register a free trial for the user after that. Let’s do it.

Create a new interactor named start_free_trial.rb in the same directory with content similar to this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class StartFreeTrial
  include Interactor

  def call
    user = context.user
    subscription = user.subscription.create(.....)

    if subscription.persisted?
      context.subscription = subscription
    else
      context.fail!
    end
  end

  def rollback
    context.subscription.destroy
  end
end

Let’s welcome our users now. Create a file named send_welcome.rb in the same directory and put something similar to it.

1
2
3
4
5
6
7
8
9
class SendWelcome
  include Interactor

  def call
    user = context.user
    subscription = context.subscription
    WelcomeMailer.send(user) if subscription.present? && subscription.free_trial == true
  end
end

Time to call our interactor in registrations_controller.rb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class RegistrationsController < ApplicationController
  def create
    result = RegisterUser.call(user_params: user_params)
    if result.success?
        flash[:success] = "You have successfully registered."
        redirect_to dashboard_path
    else
      flash[:error] = "There was a problem during registration."
      render :new
    end
  end

  private

  def user_params
    params.require(:session).permit(:email, :password)
  end
end

And yes, we have refactored the code.

Now, what was the motivation of refactoring such a small amount of code?

So let’s assume a use case that is much more complex than what we achieved in this blog. Let’s say, we also want a free trial email be sent after awarding free trial, we might also want(assuming it’s some movies platform like Netflix) to add some recommended movies based on user’s interests after registration and many more things, so the code would no longer be small and clean. This is the first point that I talked about while I was talking about the need to refactor.

The other point that is also mentioned in the gem’s documentation is, at a glance on the interactors directory, we come to know that what is the complete functionality of the application. For example.

Screen Shot 2020-03-07 at 2.38.52 PM.png

So this is all about interactors and structuring the application.