Email might be one of the most often overlooked pieces of any web application. Usually the biggest discussion around it in a project begins and ends with "and we'll send them an email when this happens...".
A little thought and some minor adjustments can help us avoid some problems that will grow as your project does. Let's talk about email as a microservice.
How Email Evolves in a Project
Most web frameworks include some semi-standard way of sending an email, ranging from the simple
mail() command to wrapping an entire templating system to abstract the pieces. These days, your next step is usually connecting to one of the popular mail delivery providers via SMTP to make sure it's delivered.
Sending via SMTP tends to create a noticeable performance hit to page response times, especially if you're sending more than one in a request. That will eventually lead to backgrounding your email deliveries to some type of job queue. A lot of frameworks have taken steps to make this process easier with some variation of a
send_later, which is very beneficial.
Eventually, that's going to lead to wanting to verify that emails were delivered. You'll have something monitoring your job queue or potentially tracking your deliveries, either via webhook integrations with an email provider or creating your own 1-pixel tracking image.
As you place more importance on ensuring your emails are delivered, you'll either create a simple buffer to allow you to tolerate a hypothetical outage, track recently sent emails and delivery confirmations, and debug delivery concerns for your users. You may one day wish to have statistics for different types of email that you send or even for specific users. You'll need to account for how to handle email bounces and spam reports, not just for future deliveries but for your application itself. As your volume increases, you'll have to ensure you're keeping SPF up to date, DKIM periodically rotated, and monitor your DMARC reports to maximize deliverability and combat phishing.
If you're delivering based on user-generated content, you're going to need some spam checks with the likes of Spam Assassin. If you don't have spam checks, you're going to want to monitor email blacklists like Spamhaus to see when you become an offender or able to respond to major email responders suddenly refusing all of your mail. That will probably lead to a change in your sending domain name at some point too, as you separate your critical messages from the user-generated ones.
Through all of this process, you've created dozens of different types of email within your application: different templates, different triggers, and different levels of importance.
You're going to start separating different types of email into different levels of delivery priority, because now your emails are probably intermingled with your application job queue, and your users are filling out support tickets because they didn't get a receipt as fast as they expected.
If you send out a lot of email at a time, then there's the potential for all of those inbound webhooks and tracking images to hit your web server at the same time. This can cause a traffic spike and probably a database spike from tracking that could have a performance impact for the users on your site.
As your application itself grows, it's inevitably going to be split into parts as well. Different parts of that application will probably need to send emails too. At that point, you'll have the option of either configuring each individual part to deal with these things itself, scattering templates everywhere... or centrally creating an email API in your monolith, cluttering your application's API requests and job queue with lots and lots of emails.
These are the factors we're looking at before we even consider things like testing, configuration settings, review from non-technical folks, etc.
But wait! I use Provider X!
"Provider X" probably does handle a lot of the things I just described. It probably handles them very well. Many of the email-as-a-service platforms provide excellent tooling around a lot of what I described, immediately when you sign up. I highly recommend investigating these services. Just remember that those services might not always be there. They might have outages, infrastructure problems, or delays. Worst case, those services might simply be discontinued.
When that happens, what do you do? Have you invested a lot of time in building around one provider? If that provider is managing your bounce/spam list, will you accidentally end up spamming users by pointing to a different service? Are you able to export that data from your provider?
These are all questions that need to be answered. One of the keys to a successful business is redundancy: having a backup plan; having a failover; not being tied to a third party for core parts of your business. And if you're on the web, odds are high that email is a core part of your business. It's definitely a balancing act, but if the redundancy is easy and relatively inexpensive to solve, it's probably a good idea.
Separate Early and Simply
You don't have to build everything I just described to separate your email into a service. That's what premature optimization looks like, and in the beginning, you're just trying to send some email.
Apply the K.I.S.S. (Keep It Simple Stupid) principle here. What exactly do I need from my email service to start with? We need to tell it that we want to send an email and then have that email sent. That's it. Don't overthink it and don't set the barrier to entry too high.
The point of separating your application email early is that the complexity of your email system is going to grow as your application grows. It's never going to be a big "now we're doing email" project, and that is exactly where the problems come from.
The slow growth of your email infrastructure within your application leads to entanglement of your email infrastructure within your application long term: clogging your queues, creating web tracking traffic, increasing load on your database from analytics, taking up space in your database, and overlapping embedded assets. These things will often come in bursts that don't reflect the real load challenges for your user-facing system.
Think of separating email early like planting a seed in its own bed. It can still grow, but you don't have to worry about the roots getting tangled.
Anatomy of a Simple Email Service
A service? Doesn't that mean I'm going to have to create a REST API and manage all the endpoints and version it and handle authentication and run a web server and manage a client library just for that!?!
Glad you asked because no, no it doesn't. People often hear "service" and think "full API," which is overkill for something that isn't public.
So do exactly the same thing. Create a separate application with a worker on its own queue and give the other parts of your application the connection information so that they can drop jobs into that queue. That's the whole API.
Use any format you like, but that Resque/Sidekiq structure is simple enough and popular enough that many other languages support sending and receiving jobs using the same structure. It's almost an unofficial standard at this point.
Now you don't even need a web server. All you need is access to a connection and the ability to write a JSON structure.
Your email service is nothing more than a single background worker with its own database. All of your templates, SMTP connection, delivery rules, different types of email, tracking, and assets will grow in their own space. But remember that you probably don't need all that to start out, so we avoid overkill and keep the barrier to good architecture at the expense of man hours and complexity to a minimum.
As a bonus, deploying updates to your email service won't affect the other parts of your application that are trying to send emails, since those are going directly to the queue anyway. You're not redeploying the queue, just the worker.
The Long View of Email as a Microservice
Over the long term, email can and will get more complicated. You'll eventually have non-technical people that may want to review the emails and make suggestions, which usually leads to coding up a "default" way to send each type of email OR you can create some type of web interface to review the template structure.
We talked about redundancy earlier, so you may want to preconfigure multiple delivery providers and define your own rules for who sends what and when. Blacklist management will become a big deal; if you're using more than one provider, you'll create problems for yourself if somebody reports you for spam on one and then you send again to another. If you have support staff, you probably aren't going to want developers to get a call every time somebody has a question about their email, so some type of web interface for support can help.
This growth can seem fairly daunting. Luckily there are some options that can give you a lot of what we're talking about without forcing you to custom build it all while preparing for potential transition. There are a lot of popular email-as-a-service providers out there, like Sendgrid, Postmark, Mailgun, and SparkPost. Sendgrid and SparkPost both have APIs to let you export and load a blacklist. So those two services in particular are optimal for redundancy if you need to switch from one to the other in case of outage, service problem, or some other reason.
Some providers offer template handling options, delivery tracking, and webhooks, but coding up solutions for each of them would probably be overkill. Likewise, they each provide mechanisms for tracking deliveries to see what happened if there's a customer inquiry, though not including the ability to resend. For future customer support, you're probably not going to want to ask support staff to check each one and then build an additional system to let them resend.
For this case, there's a handy service called Sendwithus that can bring it all together. Sendwithus lets you configure multiple email providers from a single interface, create templates on their system, and track delivery history on a per-customer basis. From a customer screen, you can see every email sent to a user with a simple button click to resend. Sending a message is as simple as making a REST call that includes your account key, the identifier for your template, and the payload for that template in simple JSON. Designers and marketing folks will have the ability to review and update all of your email templates centrally without ever needing to change code. It won't manage your blacklists for you, but if you're using Sendgrid and SparkPost, you'll be able to code up your email service to sync it back up.
Initially that setup is still probably overkill. All we really need to get started is our email service and an SMTP server. But knowing what's out there helps us create ideas for what a future plan might look like as we grow and change. Growing and changing gets a lot easier when our email was separate from the start.