<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kevin Tran</title>
    <description>The latest articles on DEV Community by Kevin Tran (@duykhoa12t).</description>
    <link>https://dev.to/duykhoa12t</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F63491%2F4e936c15-cc9c-4dcc-be52-0c433818d9e0.png</url>
      <title>DEV Community: Kevin Tran</title>
      <link>https://dev.to/duykhoa12t</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/duykhoa12t"/>
    <language>en</language>
    <item>
      <title>Tracking Email Activity from AWS Simple Email Service (SES)</title>
      <dc:creator>Kevin Tran</dc:creator>
      <pubDate>Mon, 31 Jul 2023 16:00:24 +0000</pubDate>
      <link>https://dev.to/duykhoa12t/tracking-email-activity-from-aws-simple-email-service-ses-47in</link>
      <guid>https://dev.to/duykhoa12t/tracking-email-activity-from-aws-simple-email-service-ses-47in</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The email sending function is a common feature found in almost every website. For developers, &lt;a href="https://docs.aws.amazon.com/ses/index.html"&gt;AWS SES (AWS Simple Email Service)&lt;/a&gt; is a popular choice, particularly when the website is hosted on the AWS cloud. Although AWS SES doesn't directly support email activity tracking, it does offer the possibility to create custom tracking features by integrating with other AWS services. In this post, I will explain how to build an application that tracks email sending activities using SNS HTTPs endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  SES Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configure AWS SES
&lt;/h3&gt;

&lt;p&gt;Implementing email communication with AWS SES is a straightforward task, thanks to the support of various programming languages provided by &lt;a href="https://aws.amazon.com/developer/tools/"&gt;AWS developer tools&lt;/a&gt;. &lt;br&gt;
To send an email using SES, we can directly call the AWS SDK. Here's an example using NodeJS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SESClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;AWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loadFromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./credentials.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// for testing purpose&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SendEmailCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;Destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ToAddresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;testemail@gmail.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Subject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Charset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UTF-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This is a test email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noreply@mydomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production ready, I am using the &lt;a href="https://nodemailer.com/"&gt;NodeMailer package&lt;/a&gt; instead, which provides a slightly better interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SES&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noreply@mydomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;testemail@gmail.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Subject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body in text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;HTML content&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;SES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than directly calling the SES Client, NodeMailer redirects the API call to SES through the Transport object, making it easy to switch to different transportation options. &lt;br&gt;
This flexibility is particularly helpful for developers when running the code in development and test environments. We'll explore this further in the &lt;strong&gt;Testing&lt;/strong&gt; section later on.&lt;/p&gt;
&lt;h3&gt;
  
  
  SES Dashboard
&lt;/h3&gt;

&lt;p&gt;In the code example, the app only needs to pass the necessary data, such as fromEmail, toEmail, subject, body (and attachments), to the function. &lt;br&gt;
AWS SES will then handle the entire process of sending out the email.&lt;/p&gt;

&lt;p&gt;A pressing question arises: how can we determine whether the email has been successfully delivered to the user's inbox or rejected by the user's email server?&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;SES Dashboard&lt;/strong&gt; provides valuable insights into account usage, such as the total number of sent emails, historical sending data, rejected rate, bouncing rate, and complaint rate. However, these metrics do not provide specific details about each individual email activity. A complete email system requires this information to troubleshoot any potential bounces or rejections caused by external factors. Thankfully, the SES Notifications feature, integrated with AWS Simple Notification Service and AWS Simple Queue Service, helps bridge this gap and provides the necessary information for a complete email system.&lt;/p&gt;
&lt;h3&gt;
  
  
  Complete email system
&lt;/h3&gt;

&lt;p&gt;Below is a draft design that outlines the components of an email system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hnV2vmIR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j83gphqzegg8cqka7uve.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hnV2vmIR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j83gphqzegg8cqka7uve.png" alt="Image description" width="800" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our context, after observing how SES integration is set up in various applications, we have separated the Email sending system as a standalone application. &lt;br&gt;
This approach allows other applications to avoid managing their individual AWS configurations for email communication. Instead, they can simply make an API call to the email system, which will handle the entire lifecycle of the emails.&lt;/p&gt;

&lt;p&gt;Here are some more details about each component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt;: allows client applications, SNS Subscriber (HTTP/HTTPS) and dashboard interacts with the Email System.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email Sending API&lt;/strong&gt;: provides API to trigger email, it handles the Emails Service that responses for communicate with the AWS SES, and the Recipients Service that creates/updates the recipients's profiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity API&lt;/strong&gt;: provides public endpoints for SNS subscriber to push the notification about the email's activities. The notification will be proceed later by a background job processor to the domain data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard API&lt;/strong&gt;: provides APIs for the Frontend Dashboard page to connect and render the email activities data.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  AWS SES notification config
&lt;/h2&gt;

&lt;p&gt;In each SES Domain Identity, there is a &lt;strong&gt;Notifications&lt;/strong&gt; config, where it allows to setup the SNS topic for each feedback type. Here is an example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3VOSPq2W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4jut9orvqq31od6ungj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3VOSPq2W--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4jut9orvqq31od6ungj.png" alt="Image description" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before setting up the SES notification, it's essential to create the required SNS Topics. In the example provided, separate topics are used for each notification type (bounce, complaint, and delivery), but a single topic can suffice for simpler use cases.&lt;/p&gt;

&lt;p&gt;The subsequent step involves defining the subscription for the SNS Topic, for which we'll be using the HTTPs (or HTTP) protocol in this blog post.&lt;br&gt;
AWS SNS supports various other subscription types, including SQS (event queue), email, and SMS subscriptions.&lt;/p&gt;

&lt;p&gt;To create the HTTPs subscription, select the subscription type and provide the endpoint URL. Please note that the endpoint URL should be publicly accessible on the internet, as private IP addresses won't work.&lt;/p&gt;

&lt;p&gt;In the example screenshot, the website supports creating an email hook, wherein each URL is assigned a unique identifier to distinguish email notifications from different identities. However, for simpler use cases, a general URL such as &lt;code&gt;https://mydomain.local/ses_notifications&lt;/code&gt; will suffice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YRYuXzta--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ew688xxl6gk9qmf314iu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YRYuXzta--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ew688xxl6gk9qmf314iu.png" alt="Image description" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After creating the subscription, the SNS will make a request call with &lt;strong&gt;subscription_confirm&lt;/strong&gt; message to the subscription URL.&lt;br&gt;
The application needs to store the request payload, which contains the Subscribe URL and the Subscription Token to &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html#http-subscription-confirmation-json"&gt;confirm the subscription&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To confirm the subscription, we can open the SubscribeURL on the browser or implement an automated logic that calls the &lt;a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/sns/command/ConfirmSubscriptionCommand/"&gt;ConfirmSubscriptionCommand&lt;/a&gt; as follow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;TopicArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;STRING_VALUE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;STRING_VALUE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;AuthenticateOnUnsubscribe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;STRING_VALUE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ConfirmSubscriptionCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To avoid the message be formatted, SNS subscription allows to send the raw message instead of the formatted message. However, there could be some issue when reading the request body with the raw payload if the option &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-large-payload-raw-message-delivery.html"&gt;"Enable raw message delivery"&lt;/a&gt; was enabled. For example, if the project uses ExpressJS (NodeJS), this &lt;a href="https://www.npmjs.com/package/raw-body"&gt;raw-body&lt;/a&gt; package can be used to parse the request body, the default access to &lt;code&gt;request.body&lt;/code&gt; doesn't return any data.&lt;/p&gt;

&lt;p&gt;Once the subscription is confirmed, whenever an email is sent using this SES domain identity, an &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/notification-examples.html"&gt;HTTP request&lt;/a&gt; is triggered from SNS to the preconfigured endpoint. The email system captures the request payload and processes it to determine whether the email is delivered successfully.&lt;/p&gt;

&lt;p&gt;The request payload may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"notificationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Complaint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"complaint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"complainedRecipients"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"emailAddress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"richard@example.com"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"complaintFeedbackType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abuse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"arrivalDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2016-01-27T14:59:38.237Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2016-01-27T14:59:38.237Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"feedbackId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"000001378603177f-18c07c78-fa81-4a58-9dd1-fedc3cb8f49a-000000"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"mail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;trimmed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the notificationType is "Complaint" which indicates that the email reached the recipient's mailbox but was marked as spam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core components' implementation
&lt;/h2&gt;

&lt;p&gt;I hope that I have covered the main concepts and functionalities of the Email System which integrates with AWS SES.&lt;br&gt;
In this section, we will explore the implementation of the core components in this email system.&lt;/p&gt;
&lt;h3&gt;
  
  
  Email Sending Controller
&lt;/h3&gt;

&lt;p&gt;The controller exposes an API that allows other applications to make requests to send out emails. &lt;br&gt;
gRPC is used as a default choice for microservice protocol, the gRPC proto file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;TriggerEmailAttribute&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;fieldName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;EmailAttachment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;remoteUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;optional&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;cid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;TriggerEmailRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;toEmails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;mailBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;EmailAttachment&lt;/span&gt; &lt;span class="na"&gt;attachments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;TriggerEmailAttribute&lt;/span&gt; &lt;span class="na"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;service&lt;/span&gt; &lt;span class="n"&gt;EmailsServiceController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;triggerEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TriggerEmailRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TriggerEmailResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;triggerEmail&lt;/code&gt; call receives an input of type &lt;code&gt;TriggerEmailRequest&lt;/code&gt; and returns a response with type &lt;code&gt;TriggerEmailResponse&lt;/code&gt;. The &lt;code&gt;TriggerEmailRequest&lt;/code&gt; definition includes 5 fields used to make the call to the AWS SES API. In the implementation, the controller interacts with two services: the &lt;code&gt;EmailsService&lt;/code&gt;, responsible for handling the email submission to AWS SES, and the &lt;code&gt;RecipientsService&lt;/code&gt;, which manages the creation of recipients based on the provided information. The notifications received from the SNS Subscription are later mapped to these recipients.&lt;/p&gt;

&lt;p&gt;By providing the &lt;code&gt;toEmails&lt;/code&gt; parameter in the request object, the service allows for specifying multiple email addresses to receive the same email in a single request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notification endpoints management
&lt;/h3&gt;

&lt;p&gt;As the Email sending service is used by multiple applications, each application has a distinct notification endpoint for AWS SNS to publish the email notifications.&lt;/p&gt;

&lt;p&gt;To demonstrate how to support separate endpoints for each application, the entity relationship diagram is as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oafBKSwv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z477y1x80zuccq3i1e3h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oafBKSwv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z477y1x80zuccq3i1e3h.png" alt="Image description" width="354" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When an application makes an API call to the Email Sending Service, it includes the token in the meta fields to authorize the request, enabling the service to determine the email activity based on the hook id. Subsequently, when SNS subscription publishes the notification request to a hook, the system knows how to create the corresponding activity on the application's usage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Background Job Processor
&lt;/h3&gt;

&lt;p&gt;Background jobs are crucial in modern software development for two main reasons. &lt;br&gt;
First, they handle long tasks without slowing down the main application, so users don't have to wait. &lt;br&gt;
Second, they automatically try again if something goes wrong, minimize number of tasks gets lost or left undone.&lt;/p&gt;

&lt;p&gt;The flow for the email sending and the hook notification processing looks like below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Send Email flow&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xBsya7ag--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f7sjmmc45rz662w472ul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xBsya7ag--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f7sjmmc45rz662w472ul.png" alt="Image description" width="537" height="232"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Process notification flow&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0frFITgg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0tsu375z8pd2dvqsmau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0frFITgg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0tsu375z8pd2dvqsmau.png" alt="Image description" width="552" height="542"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's clarify the difference between another task scheduler system, where the processor acts as the master node distributing tasks to worker nodes, and the current case.&lt;br&gt;
Here, the processor is a consumer class with methods that process jobs in the queue or listen to events on the queue. For more information on tasks queue documentation, you can refer to the &lt;a href="https://github.com/OptimalBits/bull"&gt;bull documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Data extraction from the notification can be done in the background as real-time updates are not required for analytic data. Parsing all the data from the payload may not be necessary, but we can store the notification for future data backfilling.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing strategy
&lt;/h2&gt;

&lt;p&gt;Email testing can be expensive if actual emails need to be triggered. Fortunately, there are third-party services like &lt;a href="https://mailcatcher.me"&gt;MailCatcher&lt;/a&gt; or &lt;a href="https://mailtrap.io/"&gt;MailTrap&lt;/a&gt; that provide email sandbox solutions, rendering sent emails in a web interface. This eliminates the need to trigger actual emails to test purposes.&lt;/p&gt;

&lt;p&gt;While third-party email sandbox services are often sufficient, I opted for a slightly improved solution with &lt;a href="https://github.com/maildev/maildev"&gt;MailDev&lt;/a&gt;. Setting up MailDev for this project was straightforward, and I'll document the configuration process here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;maildev&lt;/code&gt; can be run in Docker, here is the Docker Composer service definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;  maildev:
    image: maildev/maildev
    expose:
      - "1025"
    ports:
      - "51080:1080"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is fancy here. The &lt;code&gt;maildev&lt;/code&gt; service exposes the port 1025 for the Email App to connect, and allows the traffic to port &lt;strong&gt;1080&lt;/strong&gt; (&lt;strong&gt;51080&lt;/strong&gt; from host) to access the dashboard.&lt;/p&gt;

&lt;p&gt;Since we use &lt;strong&gt;NodeMailer&lt;/strong&gt; package, it creates a &lt;strong&gt;Transport&lt;/strong&gt; object to connect to AWS SES API. &lt;br&gt;
To make the app send emails to &lt;strong&gt;MailDev&lt;/strong&gt; in Dev environment, we need to create another transport object and hook it during the initialization.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;CreateTransportService&lt;/code&gt; takes in a &lt;code&gt;driver&lt;/code&gt; parameter, which indicates the type of transport, the code looks like below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;MailDriver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maildev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;DummyTransportation&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;StreamTransport&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;CreateTransportService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TransportServiceOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MailDriver&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;MailDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sesConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;createTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;SES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;MailDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maildev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;createTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mailDev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;DummyTransportation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;streamTransport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mailDev&lt;/code&gt; options object is simple with port and host, which can be passed into the app through a configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;maildev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1025&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;maildev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To speed up the test running time, we can defined the &lt;code&gt;DummyTransportation&lt;/code&gt; to create mock Transportation when running test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capacity calculation
&lt;/h2&gt;

&lt;p&gt;Understanding the traffic and data is important to determine the actual value of the platform we have developed.&lt;br&gt;
The application primarily focuses on handling email sending requests from various applications within the organization. While it also offers a dashboard to monitor email activities, its main functionality revolves around efficiently managing the email communication.&lt;/p&gt;

&lt;p&gt;To demonstrate, I will give an example how to calculate the capacity of the email system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sending rate: assume the app sends out 1 email per second, or 86k emails per day, 2.6M per month&lt;/li&gt;
&lt;li&gt;For each email, the system stores the activities data and the recipients data
The recipient data could be like this
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// TypeORM syntax&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;PrimaryGeneratedColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;increment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RecipientAttribute&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// other columns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row recipient takes ~20bytes, we can assume if there are 10M of users in all managed applications, the total size of the recipients table is around &lt;strong&gt;200MB&lt;/strong&gt;, which is very affordable.&lt;/p&gt;

&lt;p&gt;In the another side, the activities table could be a little bigger as the data is keep increasing. &lt;br&gt;
  Let's assume each activity takes around 30bytes, the size of the activities table would be&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in a month: 2.6M * 30bytes = 78MB&lt;/li&gt;
&lt;li&gt;in a year: 1GB&lt;/li&gt;
&lt;li&gt;in 3 year: 3GB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While storage is not a concern, the sequential scanning of the activities table may result in slow performance. On a 1GB/sec (SSD disk) speed, generating complex dashboard data queries could take up to 3 seconds without optimization. Fortunately, the use of database indexes can greatly improve query times.&lt;/p&gt;

&lt;p&gt;Overall, the application remains well-suited for the next three years, considering its current sending rate and user base.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In conclusion, I hope you found this blog post insightful and informative. &lt;br&gt;
We've discussed some of the key aspects of the centralized Email service, though it's important to acknowledge that there are many other considerations, such as integrating it with different app clients, data tracking, and storage usage, among others. &lt;br&gt;
As someone who has been involved in this project from its inception, I feel fortunate to have had the opportunity to design the application, define its features, and develop a comprehensive plan. Implementing a robust and efficient Email system can greatly enhance communication and efficiency. However, it's important to acknowledge the challenges that arise with the complexity of email configuration.&lt;/p&gt;

&lt;p&gt;I'm thrilled to share this approach with you, and I hope it inspires you to enhance your organization's platform. Thank you for taking the time to read this!&lt;/p&gt;

</description>
      <category>ses</category>
      <category>email</category>
      <category>aws</category>
      <category>nodemailer</category>
    </item>
    <item>
      <title>Setup local domain with self-signed SSL certificate</title>
      <dc:creator>Kevin Tran</dc:creator>
      <pubDate>Wed, 28 Jun 2023 20:30:59 +0000</pubDate>
      <link>https://dev.to/duykhoa12t/setup-local-domain-with-self-signed-ssl-certificate-lcb</link>
      <guid>https://dev.to/duykhoa12t/setup-local-domain-with-self-signed-ssl-certificate-lcb</guid>
      <description>&lt;h2&gt;
  
  
  Objective
&lt;/h2&gt;

&lt;p&gt;The local domain are helpful to setup the connection between the frontend and backend on the development machine.&lt;br&gt;
It is also a common solution when the actual address and port is difficult to remember.&lt;br&gt;
In this post, I will explain how to set up the local domain and config the SSL certificate on the MacOS laptop.&lt;/p&gt;
&lt;h2&gt;
  
  
  Softwares required
&lt;/h2&gt;

&lt;p&gt;In order to config the local domain name and issue the SSL cert, we'll need these tools installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;openssl&lt;/li&gt;
&lt;li&gt;MacOS Keychains app&lt;/li&gt;
&lt;li&gt;Nginx: for configuring the proxy&lt;/li&gt;
&lt;li&gt;Admin access&lt;/li&gt;
&lt;li&gt;vi: to edit the host and create the configuration files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nginx and vi are just my preference, you can use VSCode, emacs for editor and Apache, HA proxy for the proxy.&lt;/p&gt;
&lt;h2&gt;
  
  
  Create the local domain
&lt;/h2&gt;

&lt;p&gt;Setup the domain is the most simple task, you can open the hosts file with the admin privillege.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;sudo &lt;/span&gt;vi /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a line for the new domain and map it to &lt;strong&gt;127.0.0.1&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;127.0.0.1 mydomain.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you run the server inside a virtual network, for instance by running the proxy in the Virtual box or Docker, then use the IP of the virtual network interface instead.&lt;/p&gt;

&lt;p&gt;After that, you may flush the DNS cache by using this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo dscacheutil -flushcache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the Root Certificate Authority (Root CA)
&lt;/h2&gt;

&lt;p&gt;The SSL in the normal website are issued by a trusted Certificate Authority. However, for the local development, we don't need to purchase a SSL (and we don't own the domain), hence we have to setup the Root CA as well. During the handshake step, the browser checks whether the cert is valid by asking the Root CA about the cert, and if the Root CA doesn't confirm with the browser, the browser marks the site as unsafe.&lt;/p&gt;

&lt;p&gt;To generate the Root CA, we can use the &lt;strong&gt;openssl&lt;/strong&gt; tool.&lt;br&gt;
The steps are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate the root CA key&lt;/li&gt;
&lt;li&gt;Generate the root CA cert&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Personally, I suggest to create a directory to store all the files that are created during this process together, I usually create one in &lt;code&gt;/tmp/ssl&lt;/code&gt; for this purpose. All the files we create after this are in the same directory.&lt;/p&gt;

&lt;p&gt;To start with, we have to create the RootCA configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# fileName: config.conf

[req]
prompt = no
distinguished_name = req_distinguished_name
req_extensions = v3_req

[req_distinguished_name]
C = CA
ST = Kitchener
L = ON
OU = Org
CN = mydomain.local
emailAddress = dev@mydomain.local

[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = mydomain.local
DNS.2 = api.mydomain.local
DNS.2 = www.mydomain.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please modify the content of the file to match with your information and the local domain.&lt;br&gt;
The file references and examples can be found from &lt;a href="https://www.ibm.com/docs/en/hpvs/1.2.x?topic=reference-openssl-configuration-examples"&gt;IBM openSSL configuraton&lt;/a&gt; and &lt;a href="https://www.openssl.org/docs/manmaster/man5/config.html"&gt;OpenSSL document&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After creating these 2 config files, we can generate the rootCA key and cert with these 2 commands&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.pem -config config.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values of each parameters can be altered by different values, such as the number of days can be change to 365, or the algorithm can be set to sha512. For the details of each parameters, checkout the OpenSSL manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue the Self-signed SSL cert
&lt;/h2&gt;

&lt;p&gt;Create the ext file in the same directory with the content like below&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# filename: mydomain.local.ext 

authoritykeyIdentifier = keyid,issuer
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = mydomain.local
DNS.2 = api.mydomain.local
DNS.2 = www.mydomain.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, we can generate the cert key file and the cert signing request file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl genrsa -out mydomain.local.key 2048
openssl req -new -key mydomain.local.key -out mydomain.local.csr -config config.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step is to issue the cert&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl x509 \
        -in mydomain.local.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial \
        -out mydomain.local.crt -days 1024 -sha256 -extfile mydomain.local.ext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, you should have the the cert (and other files) in the temporary directory. We don't need the config file as well as the ext file.&lt;br&gt;
A common practice I usually take is to move these files to a more permanent place, such as &lt;code&gt;/etc/ssl&lt;/code&gt;, so the files aren't gone after reboot.&lt;br&gt;
To import the cert to the Apple Keychains, we can either use the Keychains UI or by running a command like below&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /etc/ssl/rootCA.pem

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Noted that we don't have to import the cert for the local domain name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config the proxy
&lt;/h2&gt;

&lt;p&gt;We can configure the SSL cert for the website from the proxy level instead of the app server. However, if you prefer to configure the cert from the app, please skip this section.&lt;/p&gt;

&lt;p&gt;I prefer to use Nginx for the proxy because it is easy to setup and the document is excellent.&lt;/p&gt;

&lt;p&gt;Here is how the nginx config look like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#filename: /opt/homebrew/etc/nginx/servers/mydomain.local.conf

upstream frontend {
   server localhost:3000;
   keepalive 100;
}

upstream backend{
   server localhost:8888;
   keepalive 100;
}

server {
  listen 443 ssl;
  server_name mydomain.local www.mydomain.local;

  ssl_certificate     /etc/ssl/mydomain.local.crt;
  ssl_certificate_key /etc/ssl/mydomain.local.key;

  # location config

  location / {
    proxy_http_version 1.1;
    proxy_pass http://frontend/;
  }

  location ~ /uploads/(file|image) {
    proxy_http_version 1.1;
    client_max_body_size 50M;
    proxy_pass http://frontend/;
  }
}

server {
  listen 443 ssl;
  server_name api.mydomain.local

  ssl_certificate     /etc/ssl/mydomain.local.crt;
  ssl_certificate_key /etc/ssl/mydomain.local.key;

  # location config

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Access-Control-Allow-Origin *;
    proxy_http_version 1.1;
    proxy_pass http://backend/;
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Nginx, we can setup complicated routing rule to for the website traffic and configure the SSL without hassle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A pro tips: If nginx is installed through **brew&lt;/strong&gt;, we can use the &lt;strong&gt;brew services&lt;/strong&gt; to make it starting automatically when the laptop is boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;This short post is a summary for the steps to setup the local domain on the local machine.&lt;br&gt;
I hope you find it useful for your works. And if you have set up the same stuffs in the Linux machine, please share the steps with me.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>configuration</category>
      <category>webdev</category>
      <category>ssl</category>
    </item>
    <item>
      <title>Rails Migration</title>
      <dc:creator>Kevin Tran</dc:creator>
      <pubDate>Tue, 23 Jun 2020 14:28:51 +0000</pubDate>
      <link>https://dev.to/duykhoa12t/rails-migration-g97</link>
      <guid>https://dev.to/duykhoa12t/rails-migration-g97</guid>
      <description>&lt;p&gt;Let's start by understanding what is Rails Migration.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is Rails migration
&lt;/h1&gt;

&lt;p&gt;Rails Migration is for changing the database structure.&lt;/p&gt;

&lt;p&gt;It is different from the Model.&lt;/p&gt;

&lt;p&gt;There are 2 types of language used to interact with the database, SQL and DDL.&lt;/p&gt;

&lt;p&gt;SQL is used to CRUD data from/to database, DDL is to change the database design.&lt;/p&gt;

&lt;p&gt;Rails Model (Active Record) works with SQL, and Rails Migration works with DDL.&lt;/p&gt;

&lt;p&gt;Rails Model supports ways to interact to with the database, while Rails Migration changes the database structure.&lt;/p&gt;

&lt;p&gt;A migration can change the name of a column in &lt;code&gt;books&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;A migration can remove the table &lt;code&gt;book_covers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A migration can rename column &lt;code&gt;sent_at&lt;/code&gt; to &lt;code&gt;delivered_at&lt;/code&gt; in the table &lt;code&gt;mail_histories&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A migration can add contraint to ensure field &lt;code&gt;name&lt;/code&gt; in &lt;code&gt;babies&lt;/code&gt; table isn't null.&lt;/p&gt;

&lt;p&gt;Those are some examples of what a migration can do.&lt;/p&gt;

&lt;p&gt;"Select those users who have more than 2 roles in the database" is done by Rails Model.&lt;/p&gt;

&lt;p&gt;"Create a book with input fields, validate the isbn to be uniq before save" is the Model responsibility.&lt;/p&gt;

&lt;p&gt;I hope this explanation works.&lt;/p&gt;

&lt;h1&gt;
  
  
  Rails migration supporting features
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Change or up/down
&lt;/h3&gt;

&lt;p&gt;Sometimes we see the method name is &lt;code&gt;up&lt;/code&gt; and &lt;code&gt;down&lt;/code&gt; and sometimes we see the method name is &lt;code&gt;change&lt;/code&gt; in the migration file.&lt;/p&gt;

&lt;p&gt;You can run &lt;code&gt;rails db:migrate&lt;/code&gt; to run all pending migrations, and &lt;code&gt;rails db:rollback&lt;/code&gt; to revert 1 migration.&lt;/p&gt;

&lt;p&gt;When the &lt;code&gt;rails db:migrate&lt;/code&gt; is executed, Rails will triggered the &lt;code&gt;up&lt;/code&gt; or &lt;code&gt;change&lt;/code&gt; method. When the command &lt;code&gt;rails db:rollback&lt;/code&gt; method is executed, Rails will try to run the reversed &lt;code&gt;change&lt;/code&gt; or &lt;code&gt;down&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;There are behaviors that aren't reversible, such as &lt;code&gt;change_column&lt;/code&gt;, &lt;code&gt;change_table&lt;/code&gt; and manual SQL triggering.&lt;/p&gt;

&lt;p&gt;If you are using those reversible behaviors, you can just use the &lt;code&gt;change&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;And if so, you can define your own &lt;code&gt;down&lt;/code&gt; migration to rollback the change. The other option is using the keyword &lt;code&gt;reversible&lt;/code&gt; to define the &lt;code&gt;up&lt;/code&gt; and &lt;code&gt;down&lt;/code&gt; behaviors. Take a look at the example below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;reversible&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;up&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;down&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The information here you can find in the &lt;a href="https://guides.rubyonrails.org/active_record_migrations.html#using-reversible"&gt;Rails guide&lt;/a&gt;. Please note that the migration can be reversed, but the data can't. When you perform one of those behaviors below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove a table&lt;/li&gt;
&lt;li&gt;Remove a column&lt;/li&gt;
&lt;li&gt;Change the column type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The change will affect to existing data. This operation can't be reversed.&lt;/p&gt;

&lt;p&gt;I want to ensure the column isn't accept null value. Hence I go ahead and add the constraint&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;change_column_null&lt;/span&gt; &lt;span class="ss"&gt;:reminders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration fails in the first try. There is null value in the database. A common solution is adding a default value for the column, then adding the null check contraint. It may end up with 2 migrations instead one. It isn't a strict rule, you can write a script (a rake task) to set default value, which is totally fine. After adding the defalt value contraint, you can rerun the migration again&lt;/p&gt;

&lt;p&gt;Rails has a better way to do so. The &lt;code&gt;change_column_null&lt;/code&gt; actually has the fourth argument to set the default value for those rows with the column to add the null constraint are holding NULL value. This is the most elegant way I found so far to deal with this situation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;change_column_null&lt;/span&gt; &lt;span class="ss"&gt;:reminders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"default reminder message"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that this won't add the &lt;code&gt;default_value&lt;/code&gt; constraint to the column.&lt;/p&gt;

&lt;p&gt;A similar situation, I want to add a unique constraint to a column, which may already contain duplication data, how should we deal with?&lt;/p&gt;

&lt;p&gt;Unfortunately, there is no straight forward way to clean up duplication data, we have to clean it up separately before adding the index. Here is a blog post talking about a safe strategy to add unique index, &lt;a href="https://medium.com/@josh_works/rails-migration-when-you-cant-add-a-uniqueness-constraint-because-you-already-have-duplicates-352a370e4b54"&gt;check it out&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The race condition is also an annoying things while perform database migration.&lt;/p&gt;

&lt;p&gt;While you try to run 2 steps migrations, for instance adding default value to a set of data, new data with null value comming in, and the migration to add the &lt;code&gt;not_null&lt;/code&gt; constraint still remains failing. The same case can happen when the team are removing duplication data, more duplicated data is comming in.&lt;/p&gt;

&lt;p&gt;Solving the race condition isn't easy too, hence for those risky operations, I think running the migration after working hours. Although we are looking at a zero time deployment, there are still places we have to take a risk assessment.&lt;/p&gt;

&lt;p&gt;You may ask why don't add those constraint from the beginning. And the answer I can come up with: &lt;em&gt;Well,...&lt;/em&gt; if we know all this, we can write a perfect software all the time, cheers.&lt;/p&gt;

&lt;p&gt;I will talk about some Rails migration features in next sections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reference
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;add_reference&lt;/span&gt; &lt;span class="ss"&gt;:books&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;

  &lt;span class="c1"&gt;# or&lt;/span&gt;

  &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:books&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method adds a column name &lt;code&gt;author_id&lt;/code&gt; to the books table.&lt;/p&gt;

&lt;p&gt;However, it doesn't make &lt;code&gt;author_id&lt;/code&gt; a foreign key to the authors table.&lt;/p&gt;

&lt;p&gt;In fact, when I started with Rails, there is no &lt;code&gt;add_reference&lt;/code&gt;. I thought the &lt;code&gt;belongs_to :author, foreign_key: 'user_id'&lt;/code&gt; added the constraint to the source model. It turned out that the key word &lt;code&gt;user_id&lt;/code&gt; here just mean the reference. It tells Rails (ActiveRecord) which column to join to the &lt;code&gt;authors&lt;/code&gt; table.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why so?
&lt;/h4&gt;

&lt;p&gt;Check out this &lt;a href="https://news.ycombinator.com/item?id=21486494"&gt;Hacker news thread&lt;/a&gt; and this &lt;a href="https://github.com/github/gh-ost/issues/331#issuecomment-266027731"&gt;Github issue&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because of some trade offs of the foreign key approach, such as performance issue, hard to combine data from many sources and extracting a subset of data, microservice approach, hence many team has decided to not go with foreign key approach to manage the relationship between models and rely on the ORM (app level) to handle this responsibility.&lt;/p&gt;

&lt;p&gt;What are the disadvantages using the ORM to manage the integrity and relationship of the database? Compare to the foreign key approach, you don't have a reliable way to ensure the integrity on the database level. Most of the database is located in separate server. Usually there will be multiple instance of the web app which writting and reading to the same database simultaneously. From time to time, the race condition and network issue can create polution to the database.&lt;/p&gt;

&lt;p&gt;However, if you need to use foreign_key, you can do that with an additional parameter &lt;code&gt;foreign_key: true&lt;/code&gt; in the &lt;code&gt;add_reference&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;add_reference&lt;/span&gt; &lt;span class="ss"&gt;:books&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are wondering why need the &lt;code&gt;add_reference&lt;/code&gt; method while what it does is adding a new column to table.&lt;/p&gt;

&lt;p&gt;Both &lt;code&gt;add_column&lt;/code&gt; and &lt;code&gt;add_reference&lt;/code&gt; are both adding column to the table, plus &lt;code&gt;add_reference&lt;/code&gt; does a bit more than that. Beside adding column, &lt;code&gt;add_reference&lt;/code&gt; provides a more friendly syntax, and secondly it maps the model to column (reference from x to y mean adding column y_id to table x). It supports polymorphic association too. Thirdly, it adds an index for new column by default, unless you know what you are doing, just leave it. And lastly, you can add a foreign key constraint by specifying &lt;code&gt;foreign_key: true&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Index
&lt;/h3&gt;

&lt;p&gt;Database index is a big topic. In this post, I just cover an extra thin iceberg layer.&lt;/p&gt;

&lt;p&gt;Generally database index speeds up database data retrieval operation.&lt;/p&gt;

&lt;p&gt;SQL query checks if index for the lookup column exist then use it instead of sequently scanning through all rows of data.&lt;/p&gt;

&lt;p&gt;If your project use email to find a user, it is definitely a good idea to have an index for the email column.&lt;/p&gt;

&lt;p&gt;You can have index for multiple columns.&lt;/p&gt;

&lt;p&gt;To add an index in Rails, you can use &lt;code&gt;add_index&lt;/code&gt; method&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;  &lt;span class="n"&gt;add_index&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:gender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:age&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"user_index_by_gender_and_age"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope you are curious enough to ask why do we need multiple columns index, and I am happy to bring the question to the next stage: when the multiple columns index doesn't work, and what is difference between multiple column index and multiple indexes?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cybertec-postgresql.com/en/combined-indexes-vs-separate-indexes-in-postgresql/"&gt;This article&lt;/a&gt; may answer your question.&lt;/p&gt;

&lt;p&gt;Another common usecase of index is to ensure a column is unique.&lt;/p&gt;

&lt;p&gt;The validation from model level can't ensure 100% as above discussion.&lt;/p&gt;

&lt;p&gt;To add the unique index, adding the &lt;code&gt;unique: true&lt;/code&gt; argument when create index.&lt;/p&gt;

&lt;p&gt;I think it is an interesting topic, let me know if you feel the same way, then I will go more details in a separate post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Postgresql extra data types
&lt;/h3&gt;

&lt;p&gt;PostgreSQL Database supports additional data types.&lt;br&gt;
By using these data type, we will depend on a specific database and the cost to migrate to different database is increased.&lt;/p&gt;

&lt;p&gt;In this section, I share the 2 most common data types from Postgres.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Array&lt;br&gt;
The array is useful to store an array of data like tags, scores, etc.&lt;br&gt;
With array support, we don't have to create extra table to store those data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Json &amp;amp; Jsonb&lt;br&gt;
Extremely useful to store a complicated data while providing sufficient operation to retrieve and update the json data.&lt;br&gt;
PostgreSQL support operators to directly access and update a key path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Range&lt;br&gt;
Define a range of data, for example age 0-18. With range type, you can perform some checking to see if ranges are overlap.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't have to design a complicated database models or adding a NoSQL Database while you can make it a column type in postgres.&lt;/p&gt;

&lt;p&gt;For an example is a todo list application. There is no need to have different models for list and item, a todos table with a list of items, each contains detail, current state and complete date that is represent as a column with jsonb type.&lt;/p&gt;

&lt;p&gt;Jsonb is faster when retrieving as well as it supports indexing, while json type is faster when writting to the database.&lt;/p&gt;

&lt;p&gt;When use jsonb with Rails, the data you are getting back is just a hash. To convert the data from json column to Ruby object, we have to handle the mapping ourselves.&lt;/p&gt;

&lt;p&gt;While writting this blog post, I find out Rails support &lt;code&gt;serialize&lt;/code&gt; and &lt;code&gt;store_accessor&lt;/code&gt; methods, which allows the model to access json attributes.&lt;/p&gt;

&lt;p&gt;This &lt;a href="https://nandovieira.com/using-postgresql-and-jsonb-with-ruby-on-rails"&gt;blog post&lt;/a&gt; provides excellent explanation on how to use these 2 methods.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For the full list of postgres data type supported by Rails, check out &lt;a href="https://edgeguides.rubyonrails.org/active_record_postgresql.html"&gt;the guide&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Multiple databases connection
&lt;/h3&gt;

&lt;p&gt;After many years, Rails has supported multiple database connection.&lt;/p&gt;

&lt;p&gt;This feature opens opportunity in many cases. Almost every medium project written on Rails are using more than 1 database.&lt;/p&gt;

&lt;p&gt;What we can do with multiple database connection?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switching connection from primary database and replica.
Reading operators are using replica while writing's are using primary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Depends on the nature of the app, some project needs heavy reading operations, By using replicas for reading, it is more ideal. The reason is because there is only 1 primary node while there can be many replicas node that copy the data from primary node and are used for reading purpose.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distributing data to multiple databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, user and profile can be stored separately. The app may not need to know about user profile when user login to the application. Another example is the product and orders. While the product data is used on the front page of the ecommerce, the order data is interests of the internal departments such as Sales, Finance and Delivery.&lt;/p&gt;

&lt;p&gt;Of course, the &lt;code&gt;foreign_key&lt;/code&gt; doesn't work with this database design approach.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrate with legacy application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rails doesn't limit to use a database for only one reading or writting purpose. While working with legacy system, we may come up with new application for new features development while putting the old app in the maintaining mode. The legacy app is still in used without new feature instroduced. From the newer application, there are some data that the new app needs to retrieve from the legacy app's database.&lt;/p&gt;

&lt;p&gt;Another approach is to use API to retrieve the data, it is quite depends on the current stage of the legacy app to decide which approach works better.&lt;/p&gt;

&lt;p&gt;The complete guide to setup multiple database connection with Rails app is &lt;a href="https://guides.rubyonrails.org/active_record_multiple_databases.html"&gt;Rails guides - Multiple Databases with Active Record&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Command line options
&lt;/h3&gt;

&lt;p&gt;From Rails 5, you can call &lt;code&gt;rails db:migrate&lt;/code&gt; and &lt;code&gt;rails db:rollback&lt;/code&gt; instead of using &lt;code&gt;rake db:migrate&lt;/code&gt; and &lt;code&gt;rake db:rollback&lt;/code&gt;, they are still Rake tasks.&lt;/p&gt;

&lt;p&gt;Rails makes it simple to just run the migrations. It will compare the last record of the table &lt;code&gt;schema_migrations&lt;/code&gt; in your database and the version in &lt;code&gt;db/schema.rb&lt;/code&gt; file to see if it is up-to-date. If the timestamp from the &lt;code&gt;schema_migrations&lt;/code&gt; table is lower than the one in db/schema.rb, Rails will execute all migration files with the version after the version of the &lt;code&gt;db/schema.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The version is a timestamp follow format &lt;strong&gt;yyyymmddhhmmss&lt;/strong&gt;, e.g 20200406022729.&lt;/p&gt;

&lt;p&gt;Because it is easier to learn with example, here are some Rails migration commands I use often&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;    rails db:migrate
    rails db:rollback &lt;span class="nv"&gt;STEP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2                   &lt;span class="c"&gt;# rollback by 2 steps, useful during development, not push yet.&lt;/span&gt;
    rails db:schema:load                       &lt;span class="c"&gt;# load databse from schema file instead.&lt;/span&gt;
    rails db:seed                              &lt;span class="c"&gt;# create seed data&lt;/span&gt;
    rails db:migrate:up &lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20200616035428 &lt;span class="c"&gt;# run a migration file, quite useful when you need it 😉&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used to add this task to perform a migration way of resetting database. We can create a custom rake task that triggers multiple existing rake task like below&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# lib/tasks/db/rebuild_dev.rake&lt;/span&gt;
    &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:db&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"This task rebuild the db for development environment"&lt;/span&gt;
      &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ss"&gt;rebuild_dev: &lt;/span&gt;&lt;span class="sx"&gt;%i(drop create migrate seed)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run it by &lt;code&gt;rails db:rebuild_dev&lt;/code&gt;. It drops the databases (ensure the rails console and rails server are quited), recreate the databases, run migration and create seed data for you, all in once.&lt;/p&gt;

&lt;p&gt;The built in &lt;code&gt;rake db:reset&lt;/code&gt; does almost the same thing, except it loads the schema file to database instead of running migration.&lt;/p&gt;

&lt;p&gt;However, you don't need this custom &lt;code&gt;db:rebuild_dev&lt;/code&gt; task because you can use the built-in &lt;code&gt;rails db:migrate:reset&lt;/code&gt; instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sql Structure schema
&lt;/h3&gt;

&lt;p&gt;It is generally same with exporting the Data definition language (DDL) from a database to a SQL file.&lt;br&gt;
  You can use the file to load into the a database to do some works without installing Rails just to run the migration.&lt;/p&gt;

&lt;p&gt;In the Rails application, Rails allows us to choose between Ruby DSL and SQL to store the snapshot of the database.&lt;/p&gt;

&lt;p&gt;Each time the migration command is executed, the &lt;code&gt;db/schema.rb&lt;/code&gt; is updated as a snapshot of your database. It is written in Ruby, it is like a aggregation of all migration files. Some migrations you may add a new column, some migration you change the column name and drop the table, but the latest state is stored in the db/schema.&lt;/p&gt;

&lt;p&gt;By running rails &lt;code&gt;db:schema:load&lt;/code&gt;, all of the existing table are wiped out and the entire database is loaded up again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remember to not use this command on the production database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Knowing the Database snapshot is written in Ruby, we know that Rails has to maintain the DSL of the schema to make it compatible with all supported Database. That requires a lot of efforts and definitely there are some lags to catch up, for instance the data type jsonb in Postgresql isn't reflected correctly for a long time.&lt;/p&gt;

&lt;p&gt;When using those cutting edge features from DB or you prefered to use SQL schema since it is more natural to you, or you have to import the sql schema to the DB to perform some works and those servers aren't accessible from outside, the SQL Structure schema may be the only choice.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Rails migration helps interacting with the Database easier.&lt;/p&gt;

&lt;p&gt;It is a thin layer but handle an extremely important component used by the rest of the app - the database.&lt;/p&gt;

&lt;p&gt;By taking advantages of this layer, the app can perform very fast, hence we may consider to understand Rails migration and Database well.&lt;/p&gt;

&lt;p&gt;The real database design is usually different from what we have taught in school. We should open mind to understand the rationale behind.&lt;/p&gt;

&lt;p&gt;NoSQL Database and reactive programming also open new ways to fix some missing features of SQL database. With new technologies, it may outdate? the migration layer in Rails. Please let me know your thoughts.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
