Avoid đ¤Śââď¸ â use Elixir đť
How Elixirâs pattern matching can help you send emails to the right users with the right content, always.
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 đŞ.
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!