Benito Serna Tips and tools for Ruby on Rails developers

How dependency injection saved me some hours of work

March 17, 2018

Do you want to know how dependency injection can help you and your team to actually doing less work?

I have an story for you…

The last week I was assigned to execute an script to register a list of investments in our system. This type of investment is just for people that already have an investment in a given project.

So, They gave me the list of investors, I prepare the list and then execute the script…

But then when I saw the console, I saw that we had this error Intercom::RateLimitExceeded: Exceeded rate limit of X in 10_seconds

Ups… apparently this project has many investors and we are sending to much events to intercom… So, I asked for a little more time, because now I was going to need to change the script…

You know, put some sleeps here, some sleeps there =( …

So, now instead of doing…

ActiveRecord::Base.transaction do
  project = Project.find_by_slug!(project_slug)

  users.each do |user_data|
    process_user_data(project, user_data)
  end
end

I changed that to…

ActiveRecord::Base.transaction do
  project = Project.find_by_slug!(project_slug)

  users.each_slice(20) do |slice|
    slice.each do |user_data|
      process_user_data(project, user_data)
    end

    log "Waiting to avoid Intercom error..."
    sleep(5)
  end
end

Not so pretty, but it can do the job, for now…

….

Some minutes later after the script have finished I was talking with the team to explain them the issue… And see if we could solve the problem in a better way…

I was telling them that te problem was that when we create an invoice, we call Intercom to tag the user as an investor on that project… But as now we were creating to many investments Intercom raised an error.

But then one of them told me… Actually, as we run this script to add an investment to people that already have an investment in that project, we don’t need to tag them on this script… Hmmm, very clever, I thought…

So, I started to think about how we can make that change… Creating an investment is not that simple… We have a function to do that, but see what we do on the script to run it…

config = {
  store: Investment,
  user_store: User,
  mailer: Mailer,
  tracker: Analytics,
  tagger: IntercomTagger,
  project_status_cacher: ProjectStatusCacher,
  investments_info_cacher: InvestmentsInfoCacher,
  account_entries_registrator: AccountEntriesRegistrator,
  total_invested_amount_calculator: TotalInvestedAmountCalculator
}

investment_data = {
  user_id: user.id,
  amount: amount,
  date: Time.current,
  investment_type: "escrow_interest"
}

Investments.create_investment(project, investment_data, config)

As you can see it talks to several objects to do it task… But then watching the configuration clearly, I saw that we were actually injecting an IntercomTagger… That is the one I need to change!, I thought.

So, I went to the declaration of that class…

class IntercomTagger
  def self.tag_user(user_id, tag)
    intercom.tags.tag(name: tag, users: [{user_id: user_id}])
  end

  def self.untag_user(user_id, tag)
    intercom.tags.untag(name: tag, users: [{user_id: user_id}])
  end

  private

  def self.intercom
    @@intercom ||= Intercom::Client.new(token: ENV["INTERCOM_TOKEN"])
  end
end

Hey, there you are Intercom::Client!….

Then I thought… would be possible to just change that IntercomTagger to a “non operational tagger”?…

So, I went to the create_investment_spec.rb to see how this function is interacting with the tagger, and I found just one test…

module Investments
  RSpec.describe "Create investment" do
    # ...
    describe "with good params" do
      # ...

      it "tags the user as investor on the project" do
        expect(tagger).to receive(:tag_user).with(user.id, "invested PROJECT-SLUG-1234")
        create_investment(project, params)
      end
    end
  end
end

Then, I move a little deeper in the production code, to investigate a little more… And I saw that inside the flow of Investemts.create_investment we have this service object CreateInvestment with…

class CreateInvestment
  def initialize(config)
    # ...
    @tagger = config.fetch(:tagger)
    # ...
  end

  def call(project, params)
    # ...
    InvestmentTags.tag_user(tagger, investment.user_id, project)
    # ...
  end
end

Hmmm, let’s see what’s inside that InvestmentsTags… Hey! this class is the one that was actually talks to the IntercomTagger

class InvestmentTags
  def self.tag_user(tagger, user_id, project)
    tagger.tag_user(user_id, tag_name(project))
  end

  def self.untag_user(tagger, user_id, project)
    tagger.untag_user(user_id, tag_name(project))
  end

  private

  def self.tag_name(project)
    # ...
  end
end

So, Yes!!

If I just change the IntercomTagger to a NonOperationalTagger, we would be able to avoid that intercom errors…

So, I just changed it…

module NonOperationalTagger
  def self.tag_user(_user_id, _tag)
  end

  def self.untag_user(_user_id, _tag)
  end
end

config = {
  # ...
  tagger: NonOperationalTagger,
  # ...
}

investment_data = {
  # ...
}

Investments.create_investment(project, investment_data, config)

Cool!

I think we actually solve the problem in a better way… for now…

Don’t you think is nice to be able to replace the things that can change for different reasons?

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.