Avoid 🤦‍♂️ — use Elixir 😻

How Elixir’s pattern matching can help you send emails to the right users with the right content, always.

Alex Loukissas
AgentRisk: Superhuman Wealth Management

--

It’s no secret that we’re huge fans of Elixir at AgentRisk. It allows us to write clean code that’s easy to reason about and, because it runs on the Erlang VM (BEAM), it’s also highly scalable. However, one of the Elixir features that we use heavily to help us sleep better at night is pattern matching. In short, what pattern matching does is that if a function call (or return value) doesn’t match what we expect, the code will crash, avoiding unintended consequences.

And that’s by design.

While this is a great feature for the more sensitive parts of our codebase that deal with managing customers’ portfolios, it also works great for everything else, including never screwing up when sending emails to users.

We’ve all gotten those emails like this one below that try to be personalized but end up reminding us that we’re just an entry in a database. I actually got the inspiration to write this after seeing two posts from Philip I. Thomas about this within just a week on his Twitter feed. We can do better 💪.

My name is Philip, but my friends call me {customer_name}.

Send personalized emails, always

We recently switched to Bamboo (an awesome and open-source Elixir email library) for sending transactional emails to our customers (e.g. emails when a new customer’s account has been approved) through SendGrid. In the rest of the post, I touch briefly on how we did this integration, focusing mostly on how using Elixir’s pattern matching we can ensure that we always send the right email with the right content.

Anti-goal: this isn’t meant to be an exhaustive guide on how to integrate SendGrid in your Elixir application. I’ll link to a couple existing guides that I found to be helpful while I was building this integration.

What is Bamboo?

It’s the easiest and most powerful way of sending emails in Elixir. It has a clean separation between email composition and email delivery. For the former, Bamboo offers an arsenal of tools to compose and format emails using functions and pipes. When it comes to email delivery, Bamboo comes with support (called adapters) for SendGrid, Mandrill, SMTP, and others. It also includes adapters that make it easy to unit test as well as view sent emails during development, without actually sending any emails. Finally, it works out of the box with Phoenix, allowing you to use views and templates to render emails. In short, it’s a powerhouse.

Installing Bamboo

Simply following the installation instructions on the project’s documentation page, you’ll be able to get going within a couple of minutes. It takes a little more configuration to set up your adapter(s). A common setup is to use your “real” delivery mechanism in your production config (i.e. in prod.exs in a typical Phoenix project), which in our case meant configuring Bamboo’s SendGridAdapter. Optionally, you may also choose to setup LocalAdapter for development and TestAdapter for testing. Since composing and delivery are decoupled in Bamboo, we won’t focus further on adapters here.

Composing emails

Let’s go back to solving the 🤦‍♂️ problem once and for all. For the purpose of this example, we’ll assume we have defined a User model which includes the first_name and email fields. These fields are first populated at signup. This is beyond the scope of this post, but Ecto provides a ton of tools to actually validate these fields when we try to insert them in the database.

Now, let’s compose an email that welcomes a new user to our service. We will use Bamboo.Email, which contains functions for composing emails. Also, let’s assume that all of our email functions live within the Acme.Tasks.Email module. Here is what this would look like:

To actually compose and send the email, we would do something like below:

But we can do better. Let’s ensure that (a) the user’s first name is non-empty and (b) that this is an active user. For the latter, we’ll assume that there is an additional state field in the User model with two possible values, “ACTIVE” and “INACTIVE”. The code above needs a couple of modifications to ensure what we want:

It may be subtle, but the code above does a couple of things: the clause when first_name != “” ensures a non-empty first name, while the additional state: “ACTIVE” key-value in the parameters ensures that this function will only be executed when the passed-in user has the value “ACTIVE” in the state field in the database.

But what happens if there is no match? Enter FunctionClauseError. This is raised by Elixir when you call a function but there is no matching function definition. Elixir doesn’t silently ignore a function call if none of the clauses match. In short, it crashes instead doing e.g. a “best effort” attempt that may lead to you sending something like this:

Going the extra mile

As I mentioned before, Bamboo has great support for testing your email code, further ensuring that you won’t have any 🤦‍♂️ issues. Using Bamboo.Test with Bamboo.TestAdapter, you can write pretty comprehensive tests, like below:

Thank you!

I really hope you found this post useful. Please let me know in the comments if you have any questions or suggestions.

Does building the future of automated wealth management and working on a super cutting-edge tech stack sound to solve really interesting problems sound interesting? Drop us a line at founders@agentrisk.com and let’s have a chat!

--

--