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?
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.