<?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>Forem: Rafael Rodrigues</title>
    <description>The latest articles on Forem by Rafael Rodrigues (@venuziano).</description>
    <link>https://forem.com/venuziano</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%2F3033873%2F8ac0de00-d617-4754-8bd5-77a5951ff0ed.jpeg</url>
      <title>Forem: Rafael Rodrigues</title>
      <link>https://forem.com/venuziano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/venuziano"/>
    <language>en</language>
    <item>
      <title>Building Robust Task Queues and Event‑Driven Workflows in NestJS with Event Bus, Redis, and Bull</title>
      <dc:creator>Rafael Rodrigues</dc:creator>
      <pubDate>Wed, 30 Jul 2025 19:26:38 +0000</pubDate>
      <link>https://forem.com/venuziano/building-robust-task-queues-and-event-driven-workflows-in-nestjs-with-event-bus-using-redis-and-bull-38c3</link>
      <guid>https://forem.com/venuziano/building-robust-task-queues-and-event-driven-workflows-in-nestjs-with-event-bus-using-redis-and-bull-38c3</guid>
      <description>&lt;p&gt;This article shows how to implement a message‑driven workflow in NestJS using its built‑in Event Bus together with Redis‑backed Bull queues. As a concrete use case, we’ll send a verification email whenever a new user registers. By using events, we decouple user registration from email delivery, making our system more modular, easier to test, and resilient to email service failures.&lt;/p&gt;

&lt;p&gt;To follow along, you should have at least a basic understanding of how NestJS and its module system work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture overview
&lt;/h2&gt;

&lt;p&gt;We glue together three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;NestJS Event Bus (CqrsModule) for in‑process notifications.&lt;/li&gt;
&lt;li&gt;Bull queues (via @nestjs/bull) backed by Redis for reliable background jobs.&lt;/li&gt;
&lt;li&gt;MailerService for actual email delivery.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this example, all components share the same Redis instance so there’s no need to operate multiple brokers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First, register the queue as a module:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Global&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;BullModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRootAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppConfigModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppEnvConfigService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;useFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppEnvConfigService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;host&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;redisHost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&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;redisPort&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="nx"&gt;BullModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerQueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MAIL_PROCESS_TOKEN&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;MailModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&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;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;useClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailQueueService&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;MailProcessor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;EmailGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BullModule&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="nc"&gt;QueueModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sets up a Redis-backed queue named mail and binds the custom EmailGateway interface to the MailQueueService implementation. These components can be adapted to suit your specific use cases. For testing purposes, this &lt;a href="https://dev.to/venuziano/mailhog-a-free-containerized-smtp-server-for-local-development-mea"&gt;article&lt;/a&gt; demonstrates how to configure a local SMTP server for email delivery. &lt;/p&gt;

&lt;p&gt;As for the Redis module, the configuration shown &lt;em&gt;here&lt;/em&gt; is based on my setup, but you can adjust it to match your own requirements.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the event:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRegistered&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;userId&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="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;readonly&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="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;username&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="kr"&gt;public&lt;/span&gt; &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;verificationCode&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="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;We publish this event any time a new user is registered. We could have any other event here, such as to send the welcome email after users verify their account, trigger a referral bonus process, log the registration activity for analytics, etc.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Publish the event within the service/use-case layer:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&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="nc"&gt;UserRegistrationService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&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;userService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;signUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;registerDto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RegisterDto&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MessageDto&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&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;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;registerDto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Account created successfully&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="c1"&gt;// any other related service here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever you call &lt;em&gt;signUp&lt;/em&gt;, an event is emitted. Later we’ll subscribe to it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enqueue jobs via an interface, here we use our interface &lt;em&gt;EmailGateway&lt;/em&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&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="nc"&gt;MailQueueService&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;EmailGateway&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InjectQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAIL_PROCESS_TOKEN&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;mailQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Queue&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;enqueueVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;to&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="nx"&gt;username&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="nx"&gt;code&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&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;mailQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;SEND_VERIFICATION_PROCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fixed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&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="c1"&gt;// any other related service here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;enqueueVerification&lt;/code&gt; schedules the verification job with retries and fixed backoff.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.add&lt;/code&gt; method supports various configuration options, choose the ones that best suit your use case. See the official documentation for full details.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the process jobs
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAIL_PROCESS_TOKEN&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="nc"&gt;MailProcessor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&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;mailService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailService&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="nd"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SEND_VERIFICATION_PROCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handleSendVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Job&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;username&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="nl"&gt;code&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="o"&gt;&amp;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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&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;span class="k"&gt;await&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;mailService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// others related processing methods&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The processor listens for incoming queue jobs and invokes the &lt;em&gt;MailService&lt;/em&gt; for each task. Defining the correct token for each process is essential, as it determines how Bull maps jobs to their respective processors.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;MailService implementation
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&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="nc"&gt;MailService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&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;mailer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailerService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;to&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="nx"&gt;username&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="nx"&gt;code&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;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;confirmationUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://yourapp.com/verify?code=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&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;mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;to&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;Confirm your email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confirmationUrl&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;Actual email dispatch is handled here. Any failure bubbles up and can be retried by Bull.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wire up the event handler:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;EventsHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;UserRegistered&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="nc"&gt;SendVerificationEvent&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;IEventHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserRegistered&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&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="na"&gt;mailQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailQueueService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserRegistered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// retry if fail to send email&lt;/span&gt;
    &lt;span class="k"&gt;return&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;mailQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueueVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verificationCode&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;This handler listens for the &lt;code&gt;UserRegistered&lt;/code&gt; event and delegates the email-sending task to the queue by calling &lt;code&gt;enqueueVerification&lt;/code&gt;. It helps separate domain logic from infrastructure concerns and allows failed attempts to be retried automatically.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Invoke the event in your service&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, you need to publish the &lt;code&gt;UserRegistered&lt;/code&gt; event at the point where a new user is created or when you issue a fresh verification code. In your user‑registration service, inject the NestJS &lt;code&gt;EventBus&lt;/code&gt; and call &lt;code&gt;publish(...)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&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="nc"&gt;UserService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&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;eventBus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventBus&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;userTokenService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserTokenService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// … other injections&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;dataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// atomic logic to handle rollbacks&lt;/span&gt;
      &lt;span class="c1"&gt;// e.g., create user, generate token, save to DB&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verificationCode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Publish event to trigger email delivery&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;eventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserRegistered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verificationCode&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;user&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;After this call, the &lt;code&gt;UserRegistered&lt;/code&gt; event flows through the NestJS Event Bus, your &lt;code&gt;SendVerificationEvent&lt;/code&gt; handler enqueues the Bull job, and &lt;code&gt;MailProcessor&lt;/code&gt; delivers the verification email in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The client calls POST /user, triggering a controller or resolver that invokes userService.create(...).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UserRegistered&lt;/code&gt; is published.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;SendVerificationEvent&lt;/code&gt; handler receives the event and enqueues a Bull job via &lt;code&gt;MailQueueService&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;MailProcessor&lt;/code&gt; picks up the job and calls &lt;code&gt;MailService.sendVerificationEmail(...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If email delivery fails, Bull retries the job up to 5 times with a 1-second fixed delay (as configured).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Benefits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Zero‑config wiring thanks to Nest’s decorators (&lt;code&gt;@OnEvent&lt;/code&gt;, &lt;code&gt;@Processor&lt;/code&gt;, &lt;code&gt;@InjectQueue&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Single infrastructure piece (Redis) powers both event transport and job persistence.&lt;/li&gt;
&lt;li&gt;Built‑in retry logic, back‑off, and dead‑letter handling.&lt;/li&gt;
&lt;li&gt;Easy horizontal scaling: add more workers or API nodes without changing code.&lt;/li&gt;
&lt;li&gt;Simple monitoring via Bull UI tools for pending/failed jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bull also provides a web-based UI to inspect and manage queues. In this example, it is accessible at &lt;a href="http://localhost:3010/queues/" rel="noopener noreferrer"&gt;http://localhost:3010/queues/&lt;/a&gt;.&lt;/p&gt;



&lt;p&gt;Through this panel, you can view job statuses, retry failed jobs, inspect logs, and examine queue parameters.&lt;/p&gt;

&lt;p&gt;To set it up, add the following code to your main.ts (or index.ts):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerBullBoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;INestApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mountPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;queueTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppEnvConfigService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;serverAdapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExpressAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExpressAdapter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;serverAdapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBasePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mountPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;adapters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BullAdapter&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queueTokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;(getQueueToken(token));
    return new BullAdapter(q);
  });

  createBullBoard(&lt;span class="si"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adapters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;serverAdapter&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt;);
  app.use(
    mountPath,
    basicAuth(&lt;span class="si"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;users&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bullUser&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bullPassword&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;challenge&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="si"&gt;}&lt;/span&gt;),
    serverAdapter.getRouter(),
  );
}

async function bootstrap(): Promise&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;INestApplication&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;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&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;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppEnvConfigService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppEnvConfigService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;registerBullBoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/queues&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="nx"&gt;MAIL_PROCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;env&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;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiPort&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;3010&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="si"&gt;}&lt;/span&gt;

void bootstrap();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;basicAuth&lt;/code&gt; middleware is optional. You can remove it if you prefer to access the Bull UI without authentication during local development.&lt;/p&gt;

&lt;p&gt;Besides Bull and the NestJS Event Bus, you could use messaging brokers like Kafka or RabbitMQ. The key question is: When should you choose a full-featured broker instead?&lt;/p&gt;

&lt;p&gt;Use a dedicated broker when your system requires cross-language communication, strict message ordering, or persistent event storage. For most web applications and background job processing, the combination of the NestJS Event Bus and Bull provides reliable and low-overhead message handling.&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>eventdriven</category>
      <category>redis</category>
      <category>node</category>
    </item>
    <item>
      <title>Terraform + AWS Free Tier, From Zero to Test Deployment: A 20‑Minute Playground</title>
      <dc:creator>Rafael Rodrigues</dc:creator>
      <pubDate>Wed, 30 Jul 2025 18:58:08 +0000</pubDate>
      <link>https://forem.com/venuziano/from-zero-to-test-deployment-terraform-aws-free-tier-a-30-minute-playground-599</link>
      <guid>https://forem.com/venuziano/from-zero-to-test-deployment-terraform-aws-free-tier-a-30-minute-playground-599</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;On July 15, 2025 AWS simplified its Free Tier into a credit-based model, offering $200 in credits to new accounts, making it easier than ever to spin up and experiment with real infrastructure at minimal cost. This example setup is intended for testing only and is not recommended for running production workloads. It is especially useful for developers who are new to AWS and Terraform and want a quick environment to start learning. In this configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Networking and access control are defined as public and SSH is enabled on all IPs.&lt;/li&gt;
&lt;li&gt;IAM roles use broad permissions rather than least-privilege best practices.&lt;/li&gt;
&lt;li&gt;RDS sizing and high availability are minimal.&lt;/li&gt;
&lt;li&gt;Terraform state is assumed to be stored locally without remote locking.&lt;/li&gt;
&lt;li&gt;Cost and load testing should be performed before handling real traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For teams and individual developers looking to validate system architectures before committing to production budgets, this new Free Tier provides a perfect playground. In this article, we explore how to leverage Terraform to provision a fully functional AWS environment, including VPC networking, EKS (Kubernetes) compute, RDS databases, ElastiCache (Redis) clusters, and logging, capable of handling a modest load of around 5,000 users. You’ll learn why Terraform is the ideal tool for repeatable, versioned infrastructure, see best-practice patterns for each component, and discover how this baseline can evolve to support millions of users down the road.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using AWS Free Tier with Terraform
&lt;/h2&gt;

&lt;p&gt;By pairing Terraform’s infrastructure‑as‑code with AWS’s $200 credits, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prototype rapidly:&lt;/strong&gt; Launch an entire VPC, cluster, database and cache in minutes. With this setup, it typically takes around 20 to 30 minutes. This timing is determined by AWS provisioning and cannot be sped up from your end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay change‑aware:&lt;/strong&gt; Preview exactly which resources will be created, modified or destroyed with terraform plan before you apply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate safely:&lt;/strong&gt; Tear down and rebuild environments without manual error, ensuring every change is tracked in Git.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To get started, install the AWS &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;CLI&lt;/a&gt; and &lt;a href="https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt;, configure your AWS credentials (grant admin role for all services).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Terraform?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency:&lt;/strong&gt; Apply the same configuration repeatedly with predictable results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modularity:&lt;/strong&gt; Break your architecture into reusable modules (VPC, EKS, RDS, ElastiCache) to simplify management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version control:&lt;/strong&gt; Keep your entire environment in Git; peer‑review changes before they touch actual infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planning:&lt;/strong&gt; Use &lt;code&gt;terraform plan&lt;/code&gt; to catch unintended changes before resources spin up (and burn credits).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get up and running in minutes, copy the Terraform files below into your working directory, then run the Terraform commands shown in the final of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure Components &amp;amp; Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. VPC &amp;amp; Networking
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File:&lt;/strong&gt; &lt;a href="https://github.com/venuziano/library-app/blob/main/src/infrastructure/cloud/terraform/main.tf" rel="noopener noreferrer"&gt;main.tf&lt;/a&gt; (VPC, subnets, Internet/NAT gateways, route tables)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Tag every resource with &lt;code&gt;Project&lt;/code&gt; and &lt;code&gt;Environment&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Split public/private subnets across at least two AZs for resilience.&lt;/li&gt;
&lt;li&gt;Enable DNS hostnames/support on your VPC for service discovery.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. EKS Cluster
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File:&lt;/strong&gt; &lt;a href="https://github.com/venuziano/library-app/blob/main/src/infrastructure/cloud/terraform/eks.tf" rel="noopener noreferrer"&gt;eks.tf&lt;/a&gt; (using &lt;code&gt;terraform-aws-modules/eks/aws&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation:&lt;/strong&gt; Leverages a battle‑tested module to provision control plane and managed node groups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Lock node groups to your home IP initially, then tighten security over time.&lt;/li&gt;
&lt;li&gt;Use auto‑scaling (&lt;code&gt;min_size&lt;/code&gt;, &lt;code&gt;max_size&lt;/code&gt;) to accommodate load spikes.&lt;/li&gt;
&lt;li&gt;Inject IAM roles and OIDC provider for fine‑grained pod permissions.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. RDS PostgreSQL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File:&lt;/strong&gt; &lt;a href="https://github.com/venuziano/library-app/blob/main/src/infrastructure/cloud/terraform/rds.tf" rel="noopener noreferrer"&gt;rds.tf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation:&lt;/strong&gt; A single &lt;code&gt;db.t3.micro&lt;/code&gt; instance with encrypted storage, automated backups, and enhanced monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Enable &lt;code&gt;performance_insights&lt;/code&gt; for query tuning.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;skip_final_snapshot = true&lt;/code&gt; only in non‑production environments; enable snapshot retention in production.&lt;/li&gt;
&lt;li&gt;Group DB subnets and parameter settings in dedicated resources for clarity.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. ElastiCache Redis (Optional - only if your API has Redis)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File:&lt;/strong&gt; &lt;a href="https://github.com/venuziano/library-app/blob/main/src/infrastructure/cloud/terraform/elasticache.tf" rel="noopener noreferrer"&gt;elasticache.tf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation:&lt;/strong&gt; A single‑node Redis cluster (&lt;code&gt;cache.t3.micro&lt;/code&gt;) with LRU eviction and key‑space notifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Use encryption in transit and at rest.&lt;/li&gt;
&lt;li&gt;Ship slow‑log and engine logs to CloudWatch for visibility.&lt;/li&gt;
&lt;li&gt;Define a subnet group spanning private subnets to isolate cache traffic.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Application &amp;amp; Security Groups
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File:&lt;/strong&gt; &lt;a href="https://github.com/venuziano/library-app/blob/main/src/infrastructure/cloud/terraform/app.tf" rel="noopener noreferrer"&gt;app.tf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation:&lt;/strong&gt; Security groups opening only necessary ports (app port 3010, SSH for maintenance).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Favor specific CIDR blocks over “0.0.0.0/0” where possible.&lt;/li&gt;
&lt;li&gt;Group common ingress/egress rules into reusable modules.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Logging &amp;amp; Monitoring
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File:&lt;/strong&gt; &lt;a href="https://github.com/venuziano/library-app/blob/main/src/infrastructure/cloud/terraform/elasticache.tf" rel="noopener noreferrer"&gt;elasticache.tf&lt;/a&gt; (CloudWatch log group defined alongside the ElastiCache resources)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation:&lt;/strong&gt; Captures ElastiCache slow‑log and engine‑log for performance analysis with a 7-day retention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Align retention settings with compliance requirements&lt;/li&gt;
&lt;li&gt;Forward logs to a centralized monitoring account as your organization grows&lt;/li&gt;
&lt;li&gt;Bind a CloudWatch log group for each major service in its respective Terraform file (for example, add log groups in &lt;code&gt;rds.tf&lt;/code&gt;, &lt;code&gt;app.tf&lt;/code&gt; and &lt;code&gt;eks.tf&lt;/code&gt;) to ensure comprehensive observability&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Variables Definition
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File:&lt;/strong&gt; &lt;a href="https://github.com/venuziano/library-app/blob/main/src/infrastructure/cloud/terraform/variables.tf" rel="noopener noreferrer"&gt;variables.tf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation:&lt;/strong&gt; Declares all input variables used by your Terraform configurations. Each variable includes a description, type, default value (where appropriate), and a &lt;code&gt;sensitive&lt;/code&gt; flag for secrets. This file ensures Terraform knows what inputs to expect and provides IDE support, type checking, and documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Practices:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Group variables by component (for example VPC, RDS, ElastiCache, EKS)&lt;/li&gt;
&lt;li&gt;Provide sensible defaults for common values and override via &lt;code&gt;.tfvars&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Mark secrets (such as database passwords) as &lt;code&gt;sensitive = true&lt;/code&gt; to avoid accidental output&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;validation&lt;/code&gt; blocks to enforce constraints (for example minimum password length)&lt;/li&gt;
&lt;li&gt;Keep descriptions concise and clear so teammates understand each variable’s purpose&lt;/li&gt;
&lt;li&gt;Avoid hard‑coding environment‑specific settings; instead inject them through &lt;code&gt;.tfvars&lt;/code&gt;, explained below.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Terraform Variables (&lt;code&gt;.tfvars&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Terraform uses a variables file to inject environment‑specific values without changing your &lt;code&gt;.tf&lt;/code&gt; files. Create a file named &lt;code&gt;terraform.tfvars&lt;/code&gt; alongside your configuration and include the following settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws_region                  = "us-east-1"
project_name                = "project-name"
environment                 = "development"
vpc_cidr                    = "10.0.0.0/16"
availability_zones          = ["us-east-1a", "us-east-1b"]
public_subnets              = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets             = ["10.0.11.0/24", "10.0.12.0/24"]
allowed_ssh_cidr            = ["0.0.0.0/0"]
db_instance_class           = "db.t3.micro"
db_allocated_storage        = 20
db_max_allocated_storage    = 100
db_name                     = "db-name"
db_username                 = "db-username"
db_password                 = "your-secure-password-here"
db_backup_retention_period  = 7
cache_node_type             = "cache.t3.micro"
eks_cluster_name            = "name-example-eks"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: Adjust these values as needed for your environment. The name of the file should be exactly &lt;code&gt;terraform.tfvars&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using a &lt;code&gt;.tfvars&lt;/code&gt; file lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep secret or environment‑specific values out of your versioned &lt;code&gt;.tf&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;Swap between development, staging and production settings by passing a different &lt;code&gt;.tfvars&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;Simplify automation by referencing a single variables file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ignoring Terraform Artifacts
&lt;/h2&gt;

&lt;p&gt;You don’t want to add sensitive or unnecessary Terraform files to your GitHub repository. Add the following entries to your &lt;code&gt;.gitignore&lt;/code&gt;, adjusting the paths to match your Terraform folder (for example &lt;code&gt;/src/infrastructure/cloud/terraform&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Terraform directories and state
/src/infrastructure/cloud/terraform/.terraform
/src/infrastructure/cloud/terraform/.terraform.lock.hcl
/src/infrastructure/cloud/terraform/.terraform.tfstate.lock.info

# Terraform variable and state files
/src/infrastructure/cloud/terraform/terraform.tfvars
/src/infrastructure/cloud/terraform/terraform.tfstate
/src/infrastructure/cloud/terraform/terraform.tfstate*

# Terraform plan files
/src/infrastructure/cloud/terraform/tfplan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Applying and Managing Terraform
&lt;/h2&gt;

&lt;p&gt;To provision this environment, inside your terraform folder, run:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;terraform init&lt;/code&gt; , followed by &lt;code&gt;terraform plan -out=tfplan&lt;/code&gt; and after &lt;code&gt;terraform apply tfplan&lt;/code&gt; , Terraform will then begin provisioning the AWS resources defined in your configuration.&lt;/p&gt;

&lt;p&gt;Others useful Terraform commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform fmt&lt;/code&gt;: keeps configuration style consistent&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform validate&lt;/code&gt;: checks for syntax errors and missing variables&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform destroy&lt;/code&gt;: destroys all resources managed by your Terraform state&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform untaint &amp;lt;resource_address&amp;gt;&lt;/code&gt;: marks a resource as tainted so it will be replaced on the next apply. Example: &lt;code&gt;terraform untaint aws_db_instance.main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform apply -replace="&amp;lt;resource_address&amp;gt;"&lt;/code&gt;: forces replacement of only the specified resource. Example: &lt;code&gt;terraform apply -replace="aws_elasticache_replication_group.main”&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform destroy -target=&amp;lt;resource_address&amp;gt;&lt;/code&gt;: destroys only the specified resource without affecting others. Example: &lt;code&gt;terraform destroy -target=aws_security_group.app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Full list &lt;a href="https://developer.hashicorp.com/terraform/cli/commands" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion &amp;amp; Scaling to Millions
&lt;/h2&gt;

&lt;p&gt;This Terraform‑driven setup, running on AWS’s new Free Tier credits gives you a turnkey environment capable of supporting roughly 5,000 moderate daily users. To scale toward millions of users, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EKS:&lt;/strong&gt; Introduce horizontal pod autoscaling, cluster autoscaler, and multiple node groups (including spot instances).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS:&lt;/strong&gt; Migrate from a single &lt;code&gt;t3.micro&lt;/code&gt; to Amazon Aurora with read replicas for read‑heavy workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ElastiCache:&lt;/strong&gt; Enable cluster‑mode with sharding and multiple replicas for high availability and throughput.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Reach:&lt;/strong&gt; Add multi‑region deployments with Route 53 latency routing and database cross‑region replicas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD &amp;amp; Observability:&lt;/strong&gt; Integrate Terraform Cloud or GitHub Actions for automated drift detection, and plug in Prometheus/Grafana for real‑time metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By treating your AWS environment as code, you maintain the agility to evolve your architecture seamlessly, and with AWS Free Tier credits still available for your initial experiments, there’s no better time to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick note:&lt;/strong&gt; If you don't want to burn your credits, you can run &lt;code&gt;terraform destroy&lt;/code&gt; after testing this setup.&lt;/p&gt;

&lt;p&gt;You can view my entire Terraform folder for reference &lt;a href="https://github.com/venuziano/library-app/tree/main/src/infrastructure/cloud/terraform" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>MailHog: a Free, Containerized SMTP Server for Local Development</title>
      <dc:creator>Rafael Rodrigues</dc:creator>
      <pubDate>Wed, 09 Jul 2025 12:54:11 +0000</pubDate>
      <link>https://forem.com/venuziano/mailhog-a-free-containerized-smtp-server-for-local-development-mea</link>
      <guid>https://forem.com/venuziano/mailhog-a-free-containerized-smtp-server-for-local-development-mea</guid>
      <description>&lt;p&gt;Are you still relying on limited third‑party SMTP services to test your email features? Hosted services often impose monthly sending caps, throttle rates, and require external network access. Self‑hosting MailHog in Docker eliminates these constraints, offers complete control over your local email pipeline, and reduces exposure to external dependencies. Unlike freemium platforms such as Mailtrap or SendGrid’s free tier, MailHog runs entirely on your machine at zero cost and without registration.&lt;/p&gt;

&lt;p&gt;Using MailHog with Docker&lt;/p&gt;

&lt;p&gt;MailHog is distributed as a lightweight Docker image. You can isolate one container per application to prevent mixing email logs across projects. By default, MailHog stores messages in memory, so every restart clears your inbox. To persist emails across restarts, configure the &lt;code&gt;MH_STORAGE&lt;/code&gt; and &lt;code&gt;MH_MAILDIR_PATH&lt;/code&gt; environment variables in your Docker Compose file and mount a volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; mailhog:
    image: mailhog/mailhog
    container_name: mailhog
    restart: unless-stopped
    environment:
      MH_STORAGE: maildir                  # switch from in-memory to maildir on disk :contentReference[oaicite:0]{index=0}
      MH_MAILDIR_PATH: /mailhog/data       # where on the container to store the maildir
    volumes:
      - ./mailhog_data:/mailhog/data
    ports:
      - '1025:1025'  # SMTP
      - '8025:8025'  # Web UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;mailhog_data&lt;/code&gt; folder at your project root. Add it to &lt;code&gt;.gitignore&lt;/code&gt; to avoid committing email logs.&lt;/p&gt;

&lt;p&gt;If you prefer a single Docker command instead of Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -d \
  --name mailhog \
  --restart unless-stopped \
  -e MH_STORAGE=maildir \
  -e MH_MAILDIR_PATH=/mailhog/data \
  -v "$(pwd)/mailhog_data:/mailhog/data" \
  -p 1025:1025 \
  -p 8025:8025 \
  mailhog/mailhog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access it using &lt;a href="http://localhost:8025/" rel="noopener noreferrer"&gt;http://localhost:8025/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgc36vec1m06v884rb7kg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgc36vec1m06v884rb7kg.png" alt="Image showing MailHog UI" width="800" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Simulating Failures with &lt;a href="https://github.com/mailhog/MailHog/blob/master/docs/JIM.md" rel="noopener noreferrer"&gt;Jim&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MailHog includes an optional chaos‑testing feature called Jim. Jim helps you test how your application handles intermittent SMTP issues: delayed deliveries, random drops, temporary network errors, and more. With Jim enabled you can verify retry logic, error handling, and user notifications under real‑world failure scenarios.&lt;/p&gt;

&lt;p&gt;Integrating MailHog with NestJS&lt;/p&gt;

&lt;p&gt;Below is an example of configuring Nest’s MailerModule to use MailHog. No authentication is required by default. Adjust &lt;code&gt;config.smtpMailHost&lt;/code&gt; and related variables to your environment variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
import { join } from 'path';

import { AppConfigModule } from '../config/app-config.module';
import { AppEnvConfigService } from '../config/environment-variables/app-env.config';
import { MailService } from 'src/application/mail/mail.service';

export function getTemplateDir(): string {
  const srcDir = join(
    process.cwd(),
    'src',
    'infrastructure',
    'mail',
    'templates',
  );
  const distDir = join(__dirname, 'templates');
  return process.env.NODE_ENV === 'production' ? distDir : srcDir;
}

@Module({
  imports: [
    MailerModule.forRootAsync({
      imports: [AppConfigModule],
      inject: [AppEnvConfigService],
      useFactory: (config: AppEnvConfigService) =&amp;gt; ({
        transport: {
          host: config.smtpMailHost,
          port: config.smtpMailPort,
          secure: config.smtpMailSecure ?? false, // optional TLS toggle
          auth: config.smtpMailUser
            ? {
                // if you need auth:
                user: config.smtpMailUser,
                pass: config.smtpMailPassword,
              }
            : undefined,
        },
        defaults: {
          from: config.smtpMailFrom,
        },
        template: {
          dir: getTemplateDir(),
          adapter: new HandlebarsAdapter(),
          options: { strict: true },
        },
      }),
    }),
  ],
  providers: [MailService],
  exports: [MailService],
})
export class MailModule {}

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

&lt;/div&gt;



&lt;p&gt;Testing MailHog Setup&lt;/p&gt;

&lt;p&gt;For a complete example of health checks and automated tests for your MailHog setup, check them out here in this &lt;a href="https://github.com/venuziano/library-app/tree/main/src/infrastructure/health-checks" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How about you? Have you tried MailHog or similar containerized SMTP? Share your experience and any tips in the comments!&lt;/p&gt;

</description>
      <category>mailhog</category>
      <category>nestjs</category>
      <category>smtp</category>
      <category>docker</category>
    </item>
    <item>
      <title>How to Optimize Search Queries in Large Databases with PostgreSQL and GIN Indexes</title>
      <dc:creator>Rafael Rodrigues</dc:creator>
      <pubDate>Sat, 12 Apr 2025 22:01:07 +0000</pubDate>
      <link>https://forem.com/venuziano/how-to-optimize-search-queries-in-large-databases-with-postgresql-and-gin-indexes-11b5</link>
      <guid>https://forem.com/venuziano/how-to-optimize-search-queries-in-large-databases-with-postgresql-and-gin-indexes-11b5</guid>
      <description>&lt;p&gt;In this article, I’m going to show you an easy and safe way to speed up search queries in large database sets using PostgreSQL. To demonstrate the process, I’ll use a common use case where you want to search through a database to find records that match a specific term.&lt;/p&gt;

&lt;p&gt;Repository link: &lt;a href="https://github.com/venuziano/high-performance-search-large-set-database" rel="noopener noreferrer"&gt;https://github.com/venuziano/high-performance-search-large-set-database&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s consider the following scenario as an example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Library admins want to search for books in a database that match a specific title or category.&lt;/li&gt;
&lt;li&gt;Their database contains over 2 million book records, with each book associated with 10 distinct categories (resulting in 20 million book-category relations).&lt;/li&gt;
&lt;li&gt;There are 100 categories in total.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To start, we can use a simple endpoint, which returns a list of books with their categories. Here’s the documentation in Swagger:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft8py5r9krlafy6sgh67b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft8py5r9krlafy6sgh67b.png" alt="Swagger endpoint" width="800" height="683"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This endpoint has pagination, sort and filter functionalities. I used the URL below in Postman, but it will also work in Swagger, Curl and other API tools:&lt;br&gt;
&lt;code&gt;/book/get/all?filter=romance&amp;amp;limit=50&amp;amp;page=1&amp;amp;order=ASC&amp;amp;sort=updated_at&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next, we need to generate the database schema and the dataset. To do so, we can use migrations that execute SQL queries and procedures to save time. If you have a look at the &lt;a href="https://github.com/venuziano/high-performance-search-large-set-database/tree/main/src/migrations" rel="noopener noreferrer"&gt;folder&lt;/a&gt; &lt;code&gt;migrations&lt;/code&gt; in the repository, it contains five migration files: the first file creates the &lt;code&gt;book&lt;/code&gt;, &lt;code&gt;category&lt;/code&gt; and &lt;code&gt;book_categories&lt;/code&gt; database tables; the second, third and fourth files create the procedures that insert data into the database; and the last file creates the database indexes. The book data (name, author, publisher, etc.) and category names are randomly generated from a predefined list in the migration files. Check them for more details if you would like to understand what’s happening in the background.&lt;/p&gt;

&lt;p&gt;Now, for testing purposes, let’s use the search terms “special book”, “category” and “romance”. When searching for “special book”, the endpoint returns 7 results among 20 millions records within 250 ms:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8v7avjq29h2sz993j9la.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8v7avjq29h2sz993j9la.png" alt=" " width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Searching for “category”, the endpoint returns 1 result within 800 ms:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7n6deoilfzy15msctwmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7n6deoilfzy15msctwmj.png" alt=" " width="800" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, when searching for “romance”, the endpoint returns around 1.2 million results within 3.5 seconds:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96er3s3hiw563qba1im3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96er3s3hiw563qba1im3.png" alt=" " width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the results, we can see that the search performance was good, despite the large amount of records in the database. This is because, instead of using built-in ORM methods and multi-table JOINs in the SELECT query, I used the following raw &lt;a href="https://github.com/venuziano/high-performance-search-large-set-database/blob/main/src/book/services/book.service.ts" rel="noopener noreferrer"&gt;SELECT&lt;/a&gt; query to fetch data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;`
   SELECT
     *,
     (
       SELECT jsonb_agg(jsonb_build_object('category', to_jsonb(c.*)))
       FROM book_categories bc
       JOIN category c ON c.id = bc.category_id
       WHERE bc.book_id = b.id
     ) AS "bookCategories"
   FROM (
     SELECT
       *
     FROM book b
     WHERE to_tsvector('english', coalesce(b.name, '') || ' ' || coalesce(b.author, '') || ' ' || coalesce(b.publisher, ''))
           @@ to_tsquery('english', $1)
     UNION
     SELECT
       *               
     FROM book b
     WHERE EXISTS (
       SELECT 1
       FROM book_categories bc
       JOIN category c ON c.id = bc.category_id
       WHERE bc.book_id = b.id
         AND to_tsvector('english', c.name) @@ to_tsquery('english', $1)
     )
   ) b
   ORDER BY b.${sortField} ${sortOrder}
   LIMIT $2 OFFSET $3;
 `

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

&lt;/div&gt;



&lt;p&gt;As there's pagination, I did the same for the COUNT query.&lt;/p&gt;

&lt;p&gt;For comparison, I created three other services using TypeORM’s built-in methods, such as &lt;code&gt;.leftJoinAndSelect()&lt;/code&gt;, &lt;code&gt;.createQueryBuilder()&lt;/code&gt; and others, one of the 'tradicional' way of using TypeORM. You can check them &lt;a href="https://github.com/venuziano/high-performance-search-large-set-database/blob/main/src/book/services/book.service.ts#L160-L225" rel="noopener noreferrer"&gt;here&lt;/a&gt;. The difference is significant: when searching for ‘romance’ (the same query I showed above), the endpoint took around 49 seconds to load.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F338r2ypyjns1f6t724fb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F338r2ypyjns1f6t724fb.png" alt=" " width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below I show the key to achieving such high performance, although results may vary depending on your machine hardware and how many fields in the database need to be scanned with the inputted search term.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In my example, I used the &lt;code&gt;jsonb_agg&lt;/code&gt; aggregate function to fetch the category data in a JSON format:
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbtr8l33t7qepskazi9sd.png" alt=" " width="800" height="745"&gt;
We use the aggregate function to group related data into one field, reducing the number of JOINs and simplifying how nested data is returned to the application. &lt;/li&gt;
&lt;li&gt;The SELECT query also uses &lt;code&gt;to_tsvector&lt;/code&gt; to convert text data into a searchable format using GIN indexes. This speeds up full-text searches by allowing the database to quickly filter out non-matching rows instead of scanning each row with pattern matching.&lt;/li&gt;
&lt;li&gt;The UNION operator combines two search conditions: one that directly searches the book’s own text fields and another that searches within its related categories. UNION inherently removes duplicates, so this optimizes each subquery to ensure that you only retrieve relevant book data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given these points, we can see that this is an efficient query because the database handles complex data transformations—for example, using the &lt;code&gt;jsonb_agg&lt;/code&gt; aggregate function—, which leverages full-text search optimizations and combines results efficiently using the UNION operator. (If you were to use regular JOINs and indexes without the listed key points, SELECT and COUNT queries would take more than 15 seconds to fetch records.) Moreover, this approach can be used for other use cases as well, such as to search for customer information and their software licenses, specific items in a store, stock names in a user’s trading history, etc.—just customize the query above according to your needs. That said, in order to avoid SQL injection, remember to always use query parameters and sanitize user input values.&lt;/p&gt;

&lt;p&gt;I encourage you to test this repository on your own. You can clone the repository, set up an &lt;code&gt;.env&lt;/code&gt; file and execute the command &lt;code&gt;docker compose up --build -d&lt;/code&gt; (refer to the &lt;a href="https://github.com/venuziano/high-performance-search-large-set-database/blob/main/README.md" rel="noopener noreferrer"&gt;Readme&lt;/a&gt; file for more details if necessary). The first initialization may take a few minutes due to the &lt;a href="https://github.com/venuziano/high-performance-search-large-set-database/blob/main/src/migrations/1743621516625-CreatePopulateBookCategoriesProcedure.ts" rel="noopener noreferrer"&gt;procedure&lt;/a&gt; that inserts the 20 million records into the book_categories table. This is what your Docker logs should look like when all the procedures have been executed:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jxzdrez32nzsmm0t6pc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jxzdrez32nzsmm0t6pc.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a related note, have you ever needed to implement pagination, sort and filter functionalities using a large database set? If yes, was your implementation efficient?&lt;/p&gt;

</description>
      <category>sql</category>
      <category>postgres</category>
      <category>fulltextsearch</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
