You know how some lessons you can only learn from experience? This is such a lesson. I'm sharing it with you so that I may remember it better.
That thing that you can't learn in a 6 month bootcamp but takes years of trial and error to hone. I don't know a name for it.
— Swizec Teller (@Swizec) January 7, 2018
Let's say you're building a system for drip campaigns. Because that's how I learned this lesson.
Your system is given a set of leads. You have to send them a series of messages, update metadata in your database, and keep the sales CRM (customer relationship management) system up-to-date so when a lead eventually replies to your message, a sales person knows what to do.
Hundreds of these systems have been developed by thousands of developers. You're building your own because startups.
The core of your system is a send_next_message
function. Something like this 👇
def send_next_message(lead) campaign = lead.campaign Twilio.send_message(lead, campaign.message) CRM.update_data(lead) campaign.advance_to_next_message end
It makes sense, right? You take the drip campaign, send the current message with Twilio, update some metadata on your CRM, then advance your campaign to the next message.
Works perfect 👌
You run it in a cronjob once a day. Like this:
def send_campaigns Lead.active.each do |lead| send_next_message.perform_async(lead) end end
You go through active leads, pick their campaign, and spawn a new background worker with perform_async
. Eventually, your queue will run the code and send your campaign.
If anything goes wrong, each campaign is isolated in its own worker. Errors don't affect other campaigns. 👌
Even better, if something goes wrong while sending the campaign, you can retry the worker.
And you've just spammed your leads and made them angry
Let's review our send_next_message
code.
def send_next_message(lead) campaign = lead.campaign Twilio.send_message(lead, campaign.message) CRM.update_data(lead) campaign.advance_to_next_message end
An error can happen at any point here.
Maybe something goes wrong when you read campaigns from the database. The worker terminates on 1st instruction, and all is good.
Things can go wrong when talking to Twilio as well. Their service could be down, your internet could act up, or something else entirely. The worker terminates on 2nd instruction, and all is still good.
What if things break when updating your CRM? Third party service, complex set of instructions with multiple API calls, a lot can go wrong.
Worker terminates on 3rd instruction.
Now you're in trouble. You've already sent a message to your user, but you weren't able to save that info. Your system doesn't know sending happened, and your CRM doesn't know either.
When the worker retries, you send the message again.
And again. And again. Until the whole worker succeeds.
😅
Your lead just got a bazillion messages, and they're upset.
Here's what you should do instead 👇
def send_next_message(lead) campaign = lead.campaign message = campaign.message CRM.update_data(lead) campaign.advance_to_next_message Twilio.send_message(lead, campaign.message) end
Fetch campaign, save the message in a local variable. Then update data in your CRM. Now if updating the CRM fails, you can keep retrying until it succeeds.
Updating the CRM was least reliable in my experience.
After updating the CRM, you update your local database. This is a reliable operation, but the most common source of programmer errors and nil
failures. It happens.
Break fast and move things, right? :)
At the end, when you're absolutely certain all your data is updated, you send the message to your lead. This is least likely to fail, and if it does, screw it.
With this approach, you are guaranteed never to spam your leads. The worst that could happen is that you advance the campaign too early and they get 4 messages instead of 5 and your sales pitch is less polished.
That's okay 🙏🏻 Your copy is robust, and annoying people is bad.
And now I know why sales automation companies have so much money. This stuff is hard. 😅
PS: There are ways to further improve your worker. You could do your DB first, then rollback if an error occurs. You could try to rollback changes on the CRM if sending fails etc. But that gets complicated fast.
Continue reading about Always put side effects last
Semantically similar articles hand-picked by GPT-4
- Build one to throw away
- More messing with time: Deduping messages between iOS and JavaScript
- How our engineering team got 12x faster using these 5 lessons about integrating 3rd-party services
- That one time a simple for-loop increased conversions by 19%
- Logging 1,721,410 events per day with Postgres, Rails, Heroku, and a bit of JavaScript
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️