94. Case Study: Email Notification Microservice
Case Study: Email Notification Microservice
Email notifications are a crucial feature in many modern applications, whether for account verification, activity alerts, or important reminders to users. As system complexity grows, the monolithic pattern starts to struggle, especially when integrating external systems or meeting high scalability demands. This is exactly where the microservice architectural style offers a modular, flexible, and scalable solution.
In this article, we’ll work through a case study of building a microservice for email notifications. I’ll guide you through the architecture design, building the core service code, integrating a message queue, and a real-world simulation. Let’s start with the big picture.
Why Use a Microservice for Email Notifications?
There are several reasons why the microservice architecture is a good fit for this feature:
- Decoupling: Email delivery runs separately from the main process (for example, user registration or order handling). A failure in one process won’t affect the main system.
- Resilience: Queueing support ensures emails are still delivered if the mail server goes down or the service experiences spikes.
- Scalability: High traffic limited to the email notification service can be handled by scaling that microservice horizontally, without touching any other service.
- Maintainability: Email logic, templates, and auditing are easy to develop independently of the core domain.
Architecture Overview
How do the other services, the message broker, and the email notifier relate to one another? Take a look at the diagram below.
flowchart LR
A(App Lain: User, Order, dsb) --|Kirim Event Notifikasi| B(Message Queue)
B --|Dequeued| C(Microservice Notifikasi Email)
C --|Kirim email| D[SMTP Server]
C --|Update log status| E(DB Notifikasi)
Explanation:
- A: Another service (for example,
UserService) sends an event to the message broker (RabbitMQ, Kafka, Redis, etc.). - B: The message broker holds the email events as a buffer.
- C: The email notification microservice picks up the event, sends the email, and writes a log to the database.
- D/E: The external SMTP/email vendor, plus the DB used for status tracking.
Designing the Email Notification Schema
The following table can be used to track every email:
| id | email_to | subject | body | status | error_message | created_at | sent_at |
|---|---|---|---|---|---|---|---|
| 1 | user@site.com | Account Activation | … | Success | NULL | 2024-06-20 19:12:21 | 2024-06-20 19:12:23 |
| 2 | user2@site.com | Reset Password | … | Failure | SMTP timeout | 2024-06-20 19:14:10 | NULL |
The status column can hold the values Pending, Success, or Failure.
Implementing the Email Microservice with Node.js & Express
Directory Structure
1microservice-email/
2├── src/
3│ ├── index.js
4│ ├── emailService.js
5│ ├── queueConsumer.js
6│ └── models/
7│ └── notification.js
8├── package.json
9└── README.mdDependencies
1{
2 "dependencies": {
3 "express": "^4.18.2",
4 "amqplib": "^0.10.3", // For RabbitMQ
5 "nodemailer": "^6.9.4", // For SMTP Email
6 "sequelize": "^6.30.0", // ORM
7 "mysql2": "^3.5.2"
8 }
9}1. Creating the Email Service (emailService.js)
1const nodemailer = require('nodemailer');
2
3const transporter = nodemailer.createTransport({
4 host: 'smtp.mailprovider.com',
5 port: 587,
6 secure: false,
7 auth: {
8 user: 'your_email@provider.com',
9 pass: 'password'
10 }
11});
12
13async function sendEmail(to, subject, body) {
14 const mailOptions = {
15 from: '"YourApp Notif" <your_email@provider.com>',
16 to,
17 subject,
18 html: body
19 };
20
21 return transporter.sendMail(mailOptions);
22}
23
24module.exports = { sendEmail };2. Defining the Notification Model (models/notification.js)
1// with Sequelize
2const { Model, DataTypes } = require('sequelize');
3const sequelize = require('../db');
4
5class Notification extends Model {}
6
7Notification.init({
8 email_to: DataTypes.STRING,
9 subject: DataTypes.STRING,
10 body: DataTypes.TEXT,
11 status: DataTypes.STRING,
12 error_message: DataTypes.STRING,
13 sent_at: DataTypes.DATE,
14}, { sequelize, modelName: 'notification' });
15
16module.exports = Notification;3. Queue Consumer (queueConsumer.js)
1const amqp = require('amqplib');
2const { sendEmail } = require('./emailService');
3const Notification = require('./models/notification');
4
5async function startConsumer() {
6 const connection = await amqp.connect('amqp://localhost');
7 const channel = await connection.createChannel();
8 const queue = 'email_notification';
9 await channel.assertQueue(queue);
10
11 channel.consume(queue, async (msg) => {
12 if(msg !== null) {
13 const { to, subject, body } = JSON.parse(msg.content.toString());
14 const notif = await Notification.create({
15 email_to: to, subject, body, status: 'PENDING', created_at: new Date()
16 });
17
18 try {
19 await sendEmail(to, subject, body);
20 notif.status = 'SUCCESS';
21 notif.sent_at = new Date();
22 } catch (err) {
23 notif.status = 'FAILURE';
24 notif.error_message = err.message;
25 }
26 await notif.save();
27 channel.ack(msg);
28 }
29 });
30}
31
32module.exports = { startConsumer };4. Running the Microservice (index.js)
1const express = require('express');
2const { startConsumer } = require('./queueConsumer');
3const app = express();
4
5app.get('/healthz', (_, res) => res.send('OK'));
6
7app.listen(8001, () => console.log('Email Service running at 8001'));
8startConsumer();Simulating Email Delivery
Suppose there’s a user registration service that wants to send an email to a user with this payload:
1const amqp = require('amqplib');
2
3async function publishEmail(to, subject, body) {
4 // Similar to the logic in the sender service
5 const conn = await amqp.connect('amqp://localhost');
6 const ch = await conn.createChannel();
7 await ch.assertQueue('email_notification');
8 ch.sendToQueue('email_notification', Buffer.from(JSON.stringify({to, subject, body})));
9 setTimeout(() => conn.close(), 500);
10}
11
12// Send Activation Email
13publishEmail('user@site.com', 'Account Activation', '<b>Welcome!</b>');As a result, the microservice will pull the message from the queue, send the email, and then update the status in the database.
Infrastructure: Failure & Scalability Scenarios
What happens if SMTP goes down, or traffic suddenly spikes?
- Retry: With a queue, messages stay buffered and are automatically retried as soon as the service recovers.
- Horizontal Scaling: Add more consumer instances (worker processes) to pull more workload from the queue (for example, with Docker/Kubernetes).
Summary and Best Practices
- Isolating the notification microservice delivers strong decoupling and reliability.
- Use a message broker (such as RabbitMQ or Kafka) for asynchronous delivery and failover.
- Logging and monitoring of emails is vital, both for debugging and for delivery SLAs.
- Avoid hardcoding SMTP credentials—always use environment variables.
Conclusion
This case study demonstrates just how powerful and maintainable an email notification microservice system can be when built in a modular, asynchronous way. With a few tweaks, this service can even be extended to SMS, Push Notifications, or other channels.
The microservice architecture is no silver bullet, but for use cases like the one above its advantages are very clear—especially in medium- to large-scale systems.
Start distributing your notifications, delegate the workload, and enjoy managing a system that becomes ever more scalable — one microservice at a time!
References:
What’s your experience implementing distributed notifications? Share it in the comments!