<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[CodeAlong]]></title><description><![CDATA[Usman Soliu: 5+ years as a software engineer, 3 focused on backend engineering. Active in open-source, innovation, & writing]]></description><link>https://code-along.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 21 Jun 2026 20:22:56 GMT</lastBuildDate><atom:link href="https://code-along.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[From Monolith to Microservices]]></title><description><![CDATA[Most systems don't start as microservices.
In fact, many of the products we use every day began as a single application connected to a single database. That is usually the right decision.
Yet, if you ]]></description><link>https://code-along.hashnode.dev/from-monolith-to-microservices</link><guid isPermaLink="true">https://code-along.hashnode.dev/from-monolith-to-microservices</guid><category><![CDATA[backend]]></category><category><![CDATA[Microservices]]></category><category><![CDATA[monolithic architecture]]></category><category><![CDATA[Modular Monolith]]></category><category><![CDATA[api]]></category><category><![CDATA[observability]]></category><category><![CDATA[logging]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Sat, 13 Jun 2026 17:08:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/5e393f6c-77d3-43ab-b003-7d8ce8d56784.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most systems don't start as microservices.</p>
<p>In fact, many of the products we use every day began as a single application connected to a single database. That is usually the right decision.</p>
<p>Yet, if you spend enough time building software, there is a good chance you will eventually hear a familiar statement:</p>
<blockquote>
<p>"We need to move to microservices."</p>
</blockquote>
<p>The statement often arrives after the system has grown beyond its original expectations.</p>
<p>The engineering team has grown.</p>
<p>Features are being delivered faster.</p>
<p>Deployments are becoming riskier.</p>
<p>Different parts of the system are competing for resources.</p>
<p>What once felt simple now feels fragile.</p>
<p>At this point, many teams begin looking at microservices as the solution.</p>
<p>Unfortunately, this is where many of them make a costly mistake.</p>
<p>They focus on splitting the application into smaller services but fail to prepare for the realities that come with distributed systems.</p>
<ul>
<li><p>Network failures.</p>
</li>
<li><p>Service dependencies.</p>
</li>
<li><p>Message duplication.</p>
</li>
<li><p>Observability challenges.</p>
</li>
<li><p>Complex debugging.</p>
</li>
<li><p>Cascading failures.</p>
</li>
</ul>
<p>I have seen engineers successfully break apart a monolith only to discover that operating ten services is significantly harder than operating one.</p>
<p>This is why I believe the conversation around microservices is often incomplete.</p>
<p>The challenge is not moving from a monolith to microservices.</p>
<p>The challenge is building APIs that continue to work when services fail, traffic spikes, dependencies become unavailable, and production incidents occur.</p>
<blockquote>
<p>"The challenge is not moving to microservices. The challenge is building APIs that survive production."</p>
</blockquote>
<p>In this article, I will walk through the practical journey from monolith to microservices. More importantly, I will show how to design APIs that are resilient, observable, and capable of surviving production.</p>
<h2>The Day Your Monolith Starts Fighting Back</h2>
<p>I often tell engineers that there is nothing wrong with a monolith.</p>
<p>In fact, I would rather inherit a well-structured monolith than a poorly designed microservices architecture.</p>
<p>Many teams rush into microservices far too early.</p>
<p>They hear success stories from companies like Netflix, Amazon, and Uber and assume microservices are the natural next step for every application.</p>
<p>They are not.</p>
<p>A monolith is usually the fastest way to deliver value during the early stages of a product.</p>
<p>Everything lives in one codebase.</p>
<p>Everything is deployed together.</p>
<p>Debugging is straightforward.</p>
<p>Transactions are simple.</p>
<p>Development is faster.</p>
<p>A typical monolithic architecture looks something like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/2dae7dd2-3834-4cd0-844c-480616b7855f.png" alt="" style="display:block;margin:0 auto" />

<p>Everything appears manageable.</p>
<p>Until it isn't.</p>
<p>Imagine an e-commerce platform.</p>
<p>At the beginning, the application supports a few thousand users.</p>
<p>The backend handles user management, orders, payments, inventory, notifications, and reporting.</p>
<p>The system performs well.</p>
<p>Deployments are easy.</p>
<p>Everyone is happy.</p>
<p>Then the business grows.</p>
<p>The marketing team launches successful campaigns.</p>
<p>Traffic increases.</p>
<p>New engineers join the team.</p>
<p>Additional features are introduced.</p>
<p>What was once a clean application slowly becoming more difficult to manage.</p>
<p>Now a change in the notification module requires deploying the entire application.</p>
<p>A bug in reporting can affect order processing.</p>
<p>A spike in product searches forces the entire application to scale, even though only one module is experiencing heavy traffic.</p>
<p>Deployment windows become stressful.</p>
<p>Engineers become increasingly cautious about making changes.</p>
<p>At some point, the monolith starts fighting back.</p>
<div>
<div>💡</div>
<div>A monolith is not a problem. A monolith that has outgrown the assumptions it was designed for is the problem.</div>
</div>

<p>Not because monoliths are bad.</p>
<p>But because the needs of the business have evolved beyond the architecture's original assumptions.</p>
<p>This is usually the point where teams begin exploring microservices.</p>
<p>The problem is that many engineers think microservices exist primarily to scale servers.</p>
<p>In my experience, that is only part of the story.</p>
<p>The real reason microservices exist is much more interesting.</p>
<p>Microservices help organizations scale teams.</p>
<blockquote>
<p>"Microservices exist to scale teams far more than they exist to scale servers."</p>
</blockquote>
<p>And understanding that distinction changes how you design systems.</p>
<p>In the next section, I'll explain why microservices exist, when they make sense, and why moving to them too early can create more problems than they solve.</p>
<h2>Why Microservices Exist (And Why Most Teams Misunderstand Them)</h2>
<p>One of the biggest misconceptions I see is the belief that microservices exist primarily to help applications handle more traffic.</p>
<p>Traffic is certainly part of the conversation, but it is rarely the root problem.</p>
<p>I have seen monolithic applications comfortably handle millions of requests.</p>
<p>I have also seen microservices architectures struggle under workloads that a well-designed monolith could have managed without difficulty.</p>
<p>The question is not:</p>
<blockquote>
<p>Can my application handle more traffic?</p>
</blockquote>
<p>The question is:</p>
<blockquote>
<p>Can my engineering team continue to deliver changes safely and efficiently as the product grows?</p>
</blockquote>
<p>That is where microservices begin to make sense.</p>
<p>As systems grow, the bottleneck often shifts from infrastructure to people.</p>
<p>A team of three engineers can comfortably work within a monolith.</p>
<p>A team of thirty engineers working on the same codebase is a very different story.</p>
<p>The challenges become less about CPUs and memory and more about coordination.</p>
<p>Teams begin stepping on each other's toes.</p>
<p>Deployments become frequent.</p>
<p>Merge conflicts increase.</p>
<p>Release cycles slow down.</p>
<p>Changes that should take hours begin taking days.</p>
<p>At this point, the architecture is no longer serving the organization effectively.</p>
<p>This is where microservices can help.</p>
<p>Not because they magically improve performance.</p>
<p>But because they allow teams to move independently.</p>
<h3>Scaling Servers vs Scaling Teams</h3>
<p>Let's consider an e-commerce platform.</p>
<p>Initially, the system consists of a single backend application responsible for:</p>
<ul>
<li><p>User Management</p>
</li>
<li><p>Product Catalog</p>
</li>
<li><p>Inventory</p>
</li>
<li><p>Payments</p>
</li>
<li><p>Notifications</p>
</li>
</ul>
<p>A single engineering team owns everything.</p>
<p>This works well.</p>
<p>But as the business grows, things become more complicated.</p>
<p>Different teams emerge.</p>
<p>One team focuses on ride operations.</p>
<p>Another team focuses on payments.</p>
<p>Another team focuses on customer communication.</p>
<p>Now imagine the payments team wants to release a critical fix.</p>
<p>Unfortunately, they must wait because another team is preparing a large deployment involving ride matching logic.</p>
<p>The two changes are completely unrelated.</p>
<p>Yet they are tightly coupled because everything lives inside the same application.</p>
<p>This is one of the first signs that the architecture may be limiting organizational growth.</p>
<p>Microservices address this problem by allowing responsibilities to be separated into independently deployable units.</p>
<p>Instead of one large application, we begin moving toward something like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/b70112b0-145c-4189-be7d-ad56db31e608.png" alt="" style="display:block;margin:0 auto" />

<p>Now each service can evolve independently.</p>
<p>The Payment Service can be updated without redeploying the Order Service.</p>
<p>The Notification Service can scale independently during peak communication periods.</p>
<p>The Notification Service can adopt a different technology stack if there is a legitimate reason to do so.</p>
<p>The architecture begins to mirror the structure of the business itself.</p>
<div>
<div>💡</div>
<div>Good service boundaries usually reflect business boundaries. If your architecture and organizational structure are constantly fighting each other, one of them is probably wrong.</div>
</div>

<h3>A Microservice Is Not Just a Smaller Application</h3>
<p>This is another mistake I frequently see.</p>
<p>Many teams take a monolith and simply split it into smaller pieces.</p>
<p>Unfortunately, this often creates what I call a distributed monolith.</p>
<p>The services are physically separated, but they remain tightly coupled.</p>
<p>Every service depends on every other service.</p>
<p>Changes ripple through the system.</p>
<p>Deployments remain coordinated.</p>
<p>Failures spread quickly.</p>
<p>The team has inherited the complexity of microservices without gaining their benefits.</p>
<p>A good microservice is not defined by its size.</p>
<p>It is defined by its responsibility.</p>
<p>Each service should own a specific business capability.</p>
<p>Examples include:</p>
<ul>
<li><p>User Service</p>
</li>
<li><p>Payment Service</p>
</li>
<li><p>Inventory Service</p>
</li>
<li><p>Notification Service</p>
</li>
<li><p>Order Service</p>
</li>
</ul>
<p>Notice that each of these represents a business function.</p>
<p>Now consider these examples:</p>
<ul>
<li><p>Utility Service</p>
</li>
<li><p>Common Service</p>
</li>
<li><p>Shared Service</p>
</li>
<li><p>Database Service</p>
</li>
</ul>
<p>These names usually indicate unclear boundaries.</p>
<p>When service boundaries are unclear, ownership becomes unclear.</p>
<p>And when ownership becomes unclear, complexity increases.</p>
<p>A useful question I often ask is:</p>
<blockquote>
<p>"What business capability disappears if this service is removed?"</p>
</blockquote>
<p>If the answer is obvious, the boundary is probably healthy.</p>
<p>If the answer is vague, the service may need to be reconsidered.</p>
<h3>The Database Conversation Nobody Likes</h3>
<p>Many teams successfully split their application into multiple services.</p>
<p>Then they connect every service to the same database.</p>
<p>At that point, they have not really solved the coupling problem.</p>
<p>They have simply moved it.</p>
<p>A shared database creates hidden dependencies.</p>
<p>One service can unintentionally break another.</p>
<p>Schema changes become risky.</p>
<p>Teams lose autonomy.</p>
<p>The database becomes the new monolith.</p>
<p>A fundamental principle of microservices is data ownership.</p>
<p>Each service should own its data.</p>
<p>For example:</p>
<p>Order Service</p>
<ul>
<li><p>Orders</p>
</li>
<li><p>Order Statuses</p>
</li>
</ul>
<p>Payment Service</p>
<ul>
<li><p>Transactions</p>
</li>
<li><p>Refunds</p>
</li>
</ul>
<p>Notification Service</p>
<ul>
<li><p>Message History</p>
</li>
<li><p>Delivery Status</p>
</li>
</ul>
<p>This separation allows teams to evolve their services independently.</p>
<p>It also introduces new challenges around consistency, which we will discuss later.</p>
<p>Every architectural decision is a trade-off.</p>
<p>Microservices are no exception.</p>
<h3>Before You Reach for Microservices</h3>
<p>Whenever someone asks me whether they should adopt microservices, I usually ask a different question:</p>
<blockquote>
<p>What problem are you trying to solve?</p>
</blockquote>
<p>If the answer is:</p>
<ul>
<li><p>We want to look modern.</p>
</li>
<li><p>Netflix uses microservices.</p>
</li>
<li><p>Everyone else is doing it.</p>
</li>
</ul>
<p>Then I strongly recommend staying with the monolith.</p>
<p>If the answer is:</p>
<ul>
<li><p>Teams are blocked by each other.</p>
</li>
<li><p>Deployments are becoming risky.</p>
</li>
<li><p>Different domains need to scale independently.</p>
</li>
<li><p>Organizational growth is creating bottlenecks.</p>
</li>
</ul>
<p>Then microservices may be worth considering.</p>
<p>The goal is never to collect services.</p>
<p>The goal is to create a system that allows the business and engineering teams to move faster without sacrificing reliability.</p>
<p>That sounds simple.</p>
<p>In practice, this is where the real challenge begins.</p>
<p>Because the moment services become independent, they need a way to communicate.</p>
<p>And communication is where many microservices architectures either succeed or fail.</p>
<p>In the next section, I'll explore the communication patterns that power microservices, when to use synchronous communication, when to use asynchronous messaging, and the trade-offs that every engineering team must understand before choosing either approach.</p>
<h2>Communication Between Services: Where Most Systems Start to Break</h2>
<p>Once you move beyond a single application, the real work begins. At this point, I usually tell engineers that the architecture is no longer the hard part. The hard part is communication.</p>
<p>Because now, instead of functions calling functions, you have services calling services over the network. And the network, unlike in-process memory, is unreliable by default.</p>
<p>I have seen systems that looked perfectly clean on paper fall apart in production simply because communication patterns were chosen without enough thought. A simple user request ends up bouncing between services, and when something goes wrong, nobody can immediately tell where the failure started.</p>
<p>This is where you start to feel the difference between synchronous and asynchronous communication, not as theory, but as lived experience.</p>
<h3>Synchronous Communication: Simple, But Fragile</h3>
<p>Synchronous communication is usually where teams begin. It feels natural because it mirrors how monoliths work. One service call another, waits for a response, and continues execution.</p>
<p>For example, in a simple order flow:</p>
<p>The Order Service receives a request, then calls the Payment Service to confirm payment before finalizing the order.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/ccf043ec-28b8-4e43-99a3-b9d12a4c95de.png" alt="" style="display:block;margin:0 auto" />

<p>On the surface, this is straightforward. It is easy to reason about, easy to implement, and easy to debug in small systems.</p>
<p>But I have learned that simplicity in distributed systems can be deceptive.</p>
<p>Because now the Order Service is no longer just responsible for orders. It is also dependent on the availability, latency, and stability of the Payment Service. If the Payment Service slows down, the entire order flow slows down. If it fails, the order flow fails with it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/749d7f46-bafd-4774-83b8-9b0776031cd1.png" alt="" style="display:block;margin:0 auto" />

<p>At scale, this creates a chain reaction. One slow service can degrade the entire system experience, even if everything else is working perfectly.</p>
<p>This is the first place I usually see teams underestimate the cost of microservices.</p>
<h3>Asynchronous Communication: Where Systems Start to Breathe</h3>
<p>At some point, teams begin to realize that not everything needs an immediate response. This is usually where asynchronous communication enters the picture.</p>
<p>Instead of calling another service directly, a service publishes an event and continues its work. Other services subscribe to that event and react independently.</p>
<p>A simple example is an order event. When an order is created, the Order Service publishes an <code>OrderCreated</code> event. The Notification Service listens and sends emails. The Analytics Service listens and updates metrics. The Inventory Service adjusts stock.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/8f1dc372-e76f-4baa-b1e2-c0c775fb6045.png" alt="" style="display:block;margin:0 auto" />

<p>The interesting part here is not just the architecture, but the shift in thinking. The Order Service no longer cares who reacts to the event or how many services are involved. It only cares that the event is published successfully.</p>
<p>This is where systems start to feel more flexible. They scale better, failures become more isolated, and services stop being tightly dependent on each other.</p>
<p>But I always caution engineers here. Asynchronous communication does not remove complexity. It moves it somewhere else.</p>
<p>Now you have to think about event ordering, duplication, eventual consistency, and debugging across time rather than across a single request flow.</p>
<p>I have seen engineers struggle more with this shift than with microservices themselves.</p>
<h3>What Real Systems Usually Look Like</h3>
<p>One of the biggest misconceptions in software architecture is that systems are either synchronous or asynchronous.</p>
<p>In reality, most successful systems are a mixture of both.</p>
<p>A ride booking request might look like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/e67a88af-1f10-4b8d-9a29-c5277eb51c94.png" alt="" style="display:block;margin:0 auto" />

<p>The payment verification remains synchronous because the passenger needs an immediate answer.</p>
<p>The notifications and analytics updates become asynchronous because they do not need to block the booking process.</p>
<p>This approach allows the system to remain responsive while reducing unnecessary dependencies.</p>
<p>Over time, I have found that this hybrid model is usually the most practical approach for production systems.</p>
<p>The goal is not to eliminate synchronous communication.</p>
<p>The goal is to reserve it for situations where immediate feedback is genuinely required.</p>
<p>The moment services begin communicating over a network, failures become inevitable.</p>
<p>Not possible.</p>
<p>Not likely.</p>
<p>Inevitable.</p>
<p>And that realization changes how you design systems.</p>
<p>In the next section, I'll explore why production-ready microservices must be designed with failure in mind from day one, and the resilience patterns that help APIs continue functioning even when parts of the system are failing.</p>
<h2>Designing for Failure: Because Production Doesn't Care About Your Architecture</h2>
<p>One of the most important lessons I learned about distributed systems is that failure is not an edge case.</p>
<p>It is a feature of the environment.</p>
<p>When engineers first move from a monolith to microservices, they often focus on service boundaries, deployment strategies, and communication patterns. Those things matter, but they are not what keeps systems running in production.</p>
<p>What keeps systems running is how they behave when things go wrong.</p>
<p>And things will go wrong.</p>
<p>A service will crash.</p>
<p>A network connection will drop.</p>
<p>A database will become unavailable.</p>
<p>A third-party provider will experience an outage.</p>
<p>A message broker will become overloaded.</p>
<p>None of these scenarios are unusual. In fact, if your system runs long enough, every single one of them will happen eventually.</p>
<p>The question is not whether failure will occur.</p>
<p>The question is how your system responds when it does.</p>
<p>I often tell engineers that the difference between a development environment and a production environment is simple:</p>
<p>Everything works in development.</p>
<p>Production introduces reality.</p>
<h3>The First Mistake: Assuming Every Request Will Succeed</h3>
<p>Imagine a ride-hailing platform.</p>
<p>A passenger requests a ride.</p>
<p>The Ride Service calls the Payment Service to validate the passenger's payment method before confirming the booking.</p>
<p>Everything works perfectly during testing.</p>
<p>The Payment Service responds within milliseconds.</p>
<p>The booking is confirmed.</p>
<p>The passenger receives a notification.</p>
<p>Everyone is happy.</p>
<p>Now imagine the same flow in production.</p>
<p>The Payment Service becomes temporarily unavailable.</p>
<p>Maybe the service is being deployed.</p>
<p>Maybe the database is under heavy load.</p>
<p>Maybe there is a network issue between services.</p>
<p>Whatever the cause, the result is the same.</p>
<p>The Ride Service is waiting for a response that never arrives.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/038a9dd3-a4ea-457f-bb95-b1b9a94247ed.png" alt="" style="display:block;margin:0 auto" />

<p>The ride booking fails.</p>
<p>Not because the Ride Service is broken.</p>
<p>Not because the passenger did anything wrong.</p>
<p>But because the system assumed every dependency would always be available.</p>
<p>That assumption is one of the fastest ways to create fragile systems.</p>
<h3>Timeouts: Teaching Systems When to Stop Waiting</h3>
<p>One of the simplest resilience patterns I use is the timeout.</p>
<p>A timeout defines how long a service is willing to wait before giving up on a request.</p>
<p>Without a timeout, a service can wait indefinitely for a dependency that may never respond.</p>
<p>This sounds harmless until dozens, hundreds, or thousands of requests begin piling up.</p>
<p>Eventually, the service runs out of resources and becomes unhealthy itself.</p>
<p>What started as a problem in one service has now spread to another.</p>
<p>I have seen incidents where a single slow dependency caused an entire system to become unresponsive simply because requests were allowed to wait forever.</p>
<p>A timeout acts as a boundary.</p>
<p>It allows a service to fail quickly and preserve resources rather than waiting endlessly for something outside its control.</p>
<p><img src="align=%22center%22" alt="" /></p>
<p>A failed request is unfortunate.</p>
<p>A system-wide outage is much worse.</p>
<h3>Retries: Giving Temporary Failures a Second Chance</h3>
<p>Not every failure is permanent.</p>
<p>Sometimes a service is restarting.</p>
<p>Sometimes a network connection briefly drops.</p>
<p>Sometimes a dependency experiences a temporary spike in traffic.</p>
<p>These situations often resolve themselves within seconds.</p>
<p>This is where retries become useful.</p>
<p>Instead of immediately failing, the service attempts the request again.</p>
<p><img src="align=%22center%22" alt="" /></p>
<p>At first glance, retries seem like an obvious improvement.</p>
<p>But I have learned that retries are dangerous when implemented carelessly.</p>
<p>Imagine a dependency that is already struggling under heavy load.</p>
<p>Now imagine thousands of services retrying requests simultaneously.</p>
<p>Instead of helping the dependency recover, the retries make the problem worse.</p>
<p>This is why I typically pair retries with exponential backoff.</p>
<p>Rather than retrying immediately, each attempt waits progressively longer before trying again.</p>
<p>The goal is not to overwhelm the failing service.</p>
<p>The goal is to give it room to recover.</p>
<h3>Circuit Breakers: Preventing Failure from Spreading</h3>
<p>One of my favorite resilience patterns is the circuit breaker.</p>
<p>The name comes from electrical systems.</p>
<p>When an electrical circuit experiences a fault, the breaker trips and temporarily stops the flow of electricity to prevent further damage.</p>
<p>The same idea applies to software.</p>
<p>Imagine a Payment Service that has been failing continuously for several minutes.</p>
<p>Without protection, every incoming ride request continues attempting payment verification.</p>
<p>The result is predictable.</p>
<p>Resources are wasted.</p>
<p>Latency increases.</p>
<p>More services become affected.</p>
<p>Instead, the circuit breaker detects repeated failures and temporarily stops sending requests.</p>
<p><img src="align=%22center%22" alt="" /></p>
<p>When the circuit is open, requests fail immediately rather than waiting for an already unhealthy dependency.</p>
<p>After a configured period, the system can attempt a small number of test requests to determine whether the dependency has recovered.</p>
<p>If recovery is successful, the circuit closes and normal traffic resumes.</p>
<p>This pattern has saved countless systems from cascading failures.</p>
<h3>Idempotency: Protecting Against Duplicate Operations</h3>
<p>There is one resilience pattern that deserves special attention because it protects something more valuable than infrastructure.</p>
<p>It protects business correctness.</p>
<p>Consider a passenger making a payment for a ride.</p>
<p>The payment request reaches the Payment Service.</p>
<p>The payment succeeds.</p>
<p>Unfortunately, the response never reaches the client because of a network interruption.</p>
<p>The client assumes the request failed and sends it again.</p>
<p>Without protection, the passenger may be charged twice.</p>
<p>I have never met a customer who enjoys discovering duplicate charges.</p>
<p>This is where idempotency becomes essential.</p>
<p>An idempotent operation produces the same outcome regardless of how many times the same request is received.</p>
<p>Instead of processing the payment repeatedly, the service recognizes that it has already handled the request and returns the original result.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/1c2bb062-cc5f-431e-b525-4640741d779a.png" alt="" style="display:block;margin:0 auto" />

<p>In distributed systems, duplicate requests are not unusual.</p>
<p>They are expected.</p>
<p>Retries, network interruptions, message redelivery, and client behavior can all produce duplicates.</p>
<p>Designing for idempotency ensures those duplicates do not become business problems.</p>
<h3>Resilience Is Not About Preventing Failure</h3>
<p>When engineers first hear about resilience patterns, they often assume the goal is to eliminate failures.</p>
<p>That is impossible.</p>
<p>No architecture, framework, cloud provider, or technology stack can guarantee that failures will never occur.</p>
<p>The purpose of resilience is not to prevent failure.</p>
<p>The purpose is to contain failure.</p>
<p>A resilient system recognizes that dependencies will occasionally become unavailable, requests will occasionally fail, and infrastructure will occasionally behave unpredictably.</p>
<p>Instead of collapsing under those conditions, it continues operating in a controlled and predictable way.</p>
<p>That shift in mindset changed how I design systems.</p>
<p>I stopped asking:</p>
<blockquote>
<p>"How do I make sure this never fails?"</p>
</blockquote>
<p>And started asking:</p>
<blockquote>
<p>"What happens when this fails?"</p>
</blockquote>
<p>The answers to that question usually reveal more about a system's production readiness than its architecture diagrams ever will.</p>
<p>Of course, surviving failure is only half the battle.</p>
<p>Even the most resilient system becomes difficult to operate if engineers cannot understand what is happening inside it.</p>
<p>And that brings us to one of the most overlooked aspects of modern software architecture: observability.</p>
<h2>Observability: Understanding What Your System Is Actually Doing</h2>
<p>The first time I worked on a system with multiple services, I thought logs would be enough.</p>
<p>I was wrong.</p>
<p>A user reported that a payment was successful, but the order was never confirmed. The Payment Service looked healthy. The Order Service looked healthy. The Notification Service looked healthy.</p>
<p>Yet somehow the workflow had failed.</p>
<p>I spent hours jumping between log files trying to piece together what happened.</p>
<p>That experience taught me something important:</p>
<p>Building distributed systems is one challenge.</p>
<p>Understanding them in production is another challenge entirely.</p>
<p>And that is where observability comes in.</p>
<p>Observability is not a monitoring tool.</p>
<p>It is not a dashboard.</p>
<p>It is not a logging library.</p>
<p>Observability is your ability to understand the internal state of a system by examining its outputs.</p>
<p>When a customer says:</p>
<blockquote>
<p>"Something isn't working."</p>
</blockquote>
<p>Observability helps you answer:</p>
<blockquote>
<p>"What happened, where did it happen, and why did it happen?"</p>
</blockquote>
<p>Without guesswork.</p>
<h3>The Three Pillars of Observability</h3>
<p>When I think about observability, I think about three things:</p>
<ul>
<li><p>Logs</p>
</li>
<li><p>Metrics</p>
</li>
<li><p>Traces</p>
</li>
</ul>
<p>Each one answers a different question.</p>
<table>
<thead>
<tr>
<th>Pillar</th>
<th>Answers</th>
</tr>
</thead>
<tbody><tr>
<td>Logs</td>
<td>What happened?</td>
</tr>
<tr>
<td>Metrics</td>
<td>How often is it happening?</td>
</tr>
<tr>
<td>Traces</td>
<td>Where did it happen?</td>
</tr>
</tbody></table>
<p>Most teams have logs.</p>
<p>Fewer teams have useful logs.</p>
<p>Even fewer teams have traces.</p>
<p>Let's fix that.</p>
<h3>Structured Logging: Stop Writing Logs for Humans Only</h3>
<p>One of the most common mistakes I see is unstructured logging.</p>
<p>Things like:</p>
<pre><code class="language-plaintext">Payment failed
</code></pre>
<p>or</p>
<pre><code class="language-plaintext">Error processing order
</code></pre>
<p>These logs seem helpful when you're developing locally.</p>
<p>They become almost useless when thousands of requests are flowing through multiple services.</p>
<p>Instead, I prefer structured logs.</p>
<p>A structured log contains context.</p>
<pre><code class="language-json">{
  "service": "payment-service",
  "orderId": "ord_123",
  "paymentId": "pay_456",
  "userId": "usr_789",
  "status": "failed",
  "reason": "insufficient_funds"
}
</code></pre>
<p>Now I can search by:</p>
<ul>
<li><p>orderId</p>
</li>
<li><p>paymentId</p>
</li>
<li><p>userId</p>
</li>
<li><p>status</p>
</li>
</ul>
<p>and immediately find relevant events.</p>
<p>In a NestJS application, I typically use Pino.</p>
<pre><code class="language-typescript">import { Logger } from 'nestjs-pino';

@Injectable()
export class PaymentService {
  constructor(
    private readonly logger: Logger,
  ) {}

  async processPayment(orderId: string) {
    this.logger.info({
      orderId,
      action: 'payment_processing_started',
    });

    // payment logic
  }
}
</code></pre>
<p>Notice what I'm logging.</p>
<p>Not sentences.</p>
<p>Context.</p>
<p>Machines can search context.</p>
<p>Humans can interpret context.</p>
<p>You need both.</p>
<h3>Correlation IDs: Following a Request Across Services</h3>
<p>Structured logs alone are not enough.</p>
<p>Imagine a request moving through four services:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/42cb78f1-7135-4e9c-9585-0f6b5ae5dc05.png" alt="" style="display:block;margin:0 auto" />

<p>Each service generates logs.</p>
<p>The challenge becomes identifying which logs belong to the same user request.</p>
<p>This is where correlation IDs become invaluable.</p>
<p>When a request enters the system, generate a unique identifier.</p>
<pre><code class="language-plaintext">x-correlation-id:
7f4a1f1b-95a4-4ec8-85ab-bd8e87c43f76
</code></pre>
<p>Every service forward that value.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/0c49ff58-6185-4a20-96b2-08859df7ac4c.png" alt="" style="display:block;margin:0 auto" />

<p>Now every log entry contains:</p>
<pre><code class="language-json">{
  "correlationId": "ABC123",
  "service": "payment-service",
  "event": "payment_completed"
}
</code></pre>
<p>When something goes wrong, I search for the correlation ID and instantly reconstruct the entire request journey.</p>
<p>This single practice has saved me more debugging time than almost any other observability technique.</p>
<h3>Implementing Correlation IDs in NestJS</h3>
<p>A simple middleware can generate and propagate correlation IDs.</p>
<pre><code class="language-typescript">import { v4 as uuid } from 'uuid';

@Injectable()
export class CorrelationIdMiddleware
implements NestMiddleware {

  use(
    req: Request,
    res: Response,
    next: NextFunction,
  ) {

    const correlationId =
      req.headers['x-correlation-id']
      || uuid();

    req['correlationId'] = correlationId;

    res.setHeader(
      'x-correlation-id',
      correlationId,
    );

    next();
  }
}
</code></pre>
<p>Every downstream service should preserve and forward the same value.</p>
<p>Think of it as a tracking number for a request.</p>
<h3>Metrics: Detecting Problems Before Users Do</h3>
<p>Logs tell me what happened.</p>
<p>Metrics tell me whether I should be worried.</p>
<p>For example:</p>
<p>A single failed payment isn't necessarily a problem.</p>
<p>But if payment failures suddenly increase from:</p>
<p><code>2%</code> to <code>35%</code></p>
<p>within five minutes,</p>
<p>I want to know immediately.</p>
<p>Some of the metrics I monitor most often include:</p>
<ul>
<li><p>Request Rate</p>
</li>
<li><p>Error Rate</p>
</li>
<li><p>Response Time</p>
</li>
<li><p>Queue Length</p>
</li>
<li><p>Database Connection Count</p>
</li>
<li><p>Memory Usage</p>
</li>
</ul>
<p>A useful mental model is this:</p>
<p>Logs help with investigations.</p>
<p>Metrics help with detection.</p>
<p>Metrics answer:</p>
<blockquote>
<p>Is something unusual happening?</p>
</blockquote>
<p>before customers start opening support tickets.</p>
<h3>Distributed Tracing: The Missing Piece</h3>
<p>Let's revisit the earlier scenario.</p>
<p>A customer says:</p>
<blockquote>
<p>"My payment succeeded but my order was never confirmed."</p>
</blockquote>
<p>Logs can help.</p>
<p>Correlation IDs can help.</p>
<p>But traces provide something even better.</p>
<p>They show the complete request journey.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/c1a43e49-4df4-4f84-b310-23870e9f38d5.png" alt="" style="display:block;margin:0 auto" />

<p>A trace doesn't just show the path.</p>
<p>It shows timing.</p>
<p>Example:</p>
<pre><code class="language-plaintext">API Gateway       12ms
Order Service     24ms
Payment Service   980ms
Notification      15ms
</code></pre>
<p>Immediately, the bottleneck becomes obvious.</p>
<p>The Payment Service is responsible for most of the latency.</p>
<p>No guessing required.</p>
<p>This is why I consider distributed tracing one of the most valuable investments for any microservices architecture.</p>
<h3>OpenTelemetry: My Preferred Starting Point</h3>
<p>When teams ask me where to begin with tracing, my answer is almost always the same:</p>
<p>Start with OpenTelemetry.</p>
<p>OpenTelemetry provides a standard way to collect:</p>
<ul>
<li><p>Traces</p>
</li>
<li><p>Metrics</p>
</li>
<li><p>Logs</p>
</li>
</ul>
<p>across services.</p>
<p>A minimal setup in NestJS might look like:</p>
<pre><code class="language-typescript">import { NodeSDK }
from '@opentelemetry/sdk-node';

const sdk = new NodeSDK();

sdk.start();
</code></pre>
<p>In a real system, traces are exported to tools such as:</p>
<ul>
<li><p>Jaeger</p>
</li>
<li><p>Tempo</p>
</li>
<li><p>Datadog</p>
</li>
<li><p>New Relic</p>
</li>
</ul>
<p>Once configured, every request begins producing trace data automatically.</p>
<p>The result is visibility that would be nearly impossible to achieve through logs alone.</p>
<h3>What Observability Looks Like in Practice</h3>
<p>A mature production environment often looks something like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/3e39f4ff-76bb-4d48-8bf0-384e37443e91.png" alt="" style="display:block;margin:0 auto" />

<p>Each tool serves a different purpose.</p>
<ul>
<li><p>OpenTelemetry collects telemetry data</p>
</li>
<li><p>Jaeger visualizes traces</p>
</li>
<li><p>Prometheus stores metrics</p>
</li>
<li><p>Grafana provides dashboards</p>
</li>
</ul>
<p>Together, they provide a complete picture of system behavior.</p>
<p>Not assumptions.</p>
<p>Not guesses.</p>
<p>Evidence.</p>
<h3>The Real Goal</h3>
<p>The goal of observability is not collecting more data.</p>
<p>I have seen teams collect enormous amounts of logs and metrics while still struggling to diagnose incidents.</p>
<p>The goal is understanding.</p>
<p>When a customer reports a problem, I want answers within minutes, not hours.</p>
<p>I want to know:</p>
<ul>
<li><p>Which service handled the request?</p>
</li>
<li><p>How long did it take?</p>
</li>
<li><p>Which dependency failed?</p>
</li>
<li><p>What happened before the failure?</p>
</li>
<li><p>What happened after the failure?</p>
</li>
</ul>
<p>The most successful engineering teams I have worked with are not necessarily the teams that experience the fewest failures.</p>
<p>They are the teams that can quickly understand and recover from them.</p>
<p>And that ability begins with observability.</p>
<p>Now that we can see what's happening inside our system, the next challenge is operating microservices responsibly at scale.</p>
<p>Because building services that communicate, recover from failures, and expose useful telemetry is only part of the journey.</p>
<p>Eventually, we need to discuss what separates a collection of services from a production-ready microservices platform.</p>
<h2>The Mistakes That Turn Microservices into Distributed Monoliths</h2>
<p>One of the reasons I enjoy discussing microservices is because most conversations focus on success stories.</p>
<p>The diagrams are clean.</p>
<p>The services are neatly separated.</p>
<p>Everything appears elegant.</p>
<p>Production rarely looks like those diagrams.</p>
<p>Over the years, I have noticed that many teams do not fail because they chose microservices.</p>
<p>They fail because they unknowingly recreate the same coupling they were trying to escape.</p>
<p>They trade one monolith for a distributed monolith.</p>
<p>And the worst part is that they often do not realize it until the system becomes difficult to evolve.</p>
<h3>Mistake #1: Splitting Services Without Clear Boundaries</h3>
<p>The first sign of trouble usually appears during service decomposition.</p>
<p>A team takes a monolith and begins creating services:</p>
<ul>
<li><p>User Service</p>
</li>
<li><p>Order Service</p>
</li>
<li><p>Product Service</p>
</li>
</ul>
<p>Good so far.</p>
<p>Then:</p>
<ul>
<li><p>Utility Service</p>
</li>
<li><p>Common Service</p>
</li>
<li><p>Shared Service</p>
</li>
</ul>
<p>That is usually where I start asking questions.</p>
<p>A service should represent a business capability, not a collection of unrelated helper functions.</p>
<p>When service boundaries are unclear, ownership becomes unclear.</p>
<p>When ownership becomes unclear, complexity follows.</p>
<p>One question I often ask is:</p>
<blockquote>
<p>"If this service disappeared tomorrow, what business capability would disappear with it?"</p>
</blockquote>
<p>If nobody can answer confidently, the boundary probably needs more thought.</p>
<h3>Mistake #2: Sharing the Same Database</h3>
<p>One of the most common anti-patterns I see is this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/c4b45854-f042-4cd0-ba9a-0abecdef29aa.png" alt="" style="display:block;margin:0 auto" />

<p>Technically, the services are separate.</p>
<p>Operationally, they are still coupled.</p>
<p>The database becomes the new monolith.</p>
<p>A schema change from one team can break another team's service.</p>
<p>Independent deployments become difficult.</p>
<p>Autonomy disappears.</p>
<p>Microservices should own their data.</p>
<p>That ownership is what enables true independence.</p>
<h3>Mistake #3: Making Everything Synchronous</h3>
<p>This usually starts innocently.</p>
<p>A service needs information from another service.</p>
<p>A REST call is added.</p>
<p>Then another.</p>
<p>Then another.</p>
<p>Eventually, a single request looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f4a91545ee1ba597542e056/b6bea988-7407-4975-b9a2-7350be4203ab.png" alt="" style="display:block;margin:0 auto" />

<p>The architecture may look organized.</p>
<p>The latency certainly isn't.</p>
<p>One slow dependency can affect the entire chain.</p>
<p>I have seen systems where a single request required seven synchronous service calls before a response could be returned.</p>
<p>That is rarely sustainable.</p>
<h3>Mistake #4: Ignoring Observability Until Production</h3>
<p>This mistake usually reveals itself during the first serious incident.</p>
<p>A customer reports a problem.</p>
<p>Logs exist.</p>
<p>Metrics exist.</p>
<p>But nobody can explain what actually happened.</p>
<p>At that moment, observability stops feeling optional.</p>
<p>It becomes a necessity.</p>
<p>I strongly prefer introducing structured logging, metrics, and tracing before the first production deployment.</p>
<p>Retrofitting observability later is significantly harder.</p>
<h3>Mistake #5: Choosing Microservices Too Early</h3>
<p>This is probably my most controversial opinion.</p>
<p>Many systems should remain monoliths.</p>
<p>A well-structured monolith is not a failure.</p>
<p>It is often the simplest and most effective solution.</p>
<p>I would rather maintain a healthy monolith than operate ten unnecessary services.</p>
<p>Microservices introduce operational complexity.</p>
<p>The benefits must justify that complexity.</p>
<p>If they do not, the monolith is usually the better choice.</p>
<h2>The Goal Was Never Microservices</h2>
<p>If there is one thing I want every engineer reading this to understand, it is this:</p>
<p>Microservices were never the goal.</p>
<p>They were never the destination.</p>
<p>They were never the measure of good architecture.</p>
<p>They are simply one possible answer to a deeper problem.</p>
<p>Over the years, I have seen engineers obsess over architecture diagrams. I have seen teams celebrate the moment they “moved to microservices” as though it represents a level of engineering maturity.</p>
<p>But I have also seen what happens after the excitement fades.</p>
<p>Systems become harder to understand.</p>
<p>Deployments become more delicate.</p>
<p>Debugging becomes slower.</p>
<p>Incidents take longer to resolve.</p>
<p>And slowly, quietly, teams begin to realize that they have not eliminated complexity.</p>
<p>They have relocated it.</p>
<h3>What Actually Matters in Production Systems</h3>
<p>When I reflect on the systems, I consider truly well-designed, I notice a pattern.</p>
<p>It has very little to do with whether they are monoliths or microservices.</p>
<p>Instead, it comes down to a few practical questions:</p>
<p>Can the system evolve without breaking itself?</p>
<p>Can teams ship changes without fear?</p>
<p>Can failures be understood quickly?</p>
<p>Can issues be isolated instead of spreading?</p>
<p>Can the system recover gracefully when things go wrong?</p>
<p>Those are the real indicators of good architecture.</p>
<p>Not the number of services.</p>
<p>Not the sophistication of the stack.</p>
<p>Not the popularity of the tools.</p>
<h3>A System Is Only as Strong as Its Weakest Assumption</h3>
<p>One lesson I keep returning to is that most production failures are not caused by broken code.</p>
<p>They are caused by incorrect assumptions.</p>
<p>Assumptions like:</p>
<ul>
<li><p>A dependency will always be available</p>
</li>
<li><p>A network call will always succeed</p>
</li>
<li><p>A service will always respond within a reasonable time</p>
</li>
<li><p>A message will only be processed once</p>
</li>
<li><p>Data will always be consistent across services</p>
</li>
</ul>
<p>Microservices amplify these assumptions because they force them to cross boundaries.</p>
<p>That is why we spent so much time talking about:</p>
<ul>
<li><p>Timeouts</p>
</li>
<li><p>Retries</p>
</li>
<li><p>Circuit breakers</p>
</li>
<li><p>Idempotency</p>
</li>
<li><p>Observability</p>
</li>
</ul>
<p>These are not “advanced topics.”</p>
<p>They are survival mechanisms.</p>
<h3>What I Tell Engineers Who Want to “Do Microservices Right”</h3>
<p>When engineers ask me how to properly adopt microservices, I usually do not start with technology.</p>
<p>I start with questions.</p>
<p>Do you understand your domain well enough to split it safely?</p>
<p>Can your team operate multiple services independently?</p>
<p>Do you have visibility into what happens in production today?</p>
<p>Can you trace a single request across your system without guessing?</p>
<p>Can you recover quickly when something fails?</p>
<p>If the answer to most of these questions is no, then microservices will not solve the problem.</p>
<p>They will simply expose it.</p>
<h3>Architecture Is a Reflection of Discipline</h3>
<p>One thing I have come to believe strongly is that architecture does not create discipline.</p>
<p>It reveals it.</p>
<p>A well-disciplined team can build a stable monolith.</p>
<p>A well-disciplined team can operate microservices at scale.</p>
<p>An undisciplined team can struggle with both.</p>
<p>The difference is not the architecture.</p>
<p>The difference is the engineering mindset behind it.</p>
<h3>The Real Evolution</h3>
<p>If I step back and look at the journey we have walked through in this article, it is not really a story about monoliths or microservices.</p>
<p>It is a story about maturity.</p>
<p>We started with a simple system.</p>
<p>We introduced boundaries.</p>
<p>We handled communication.</p>
<p>We prepared for failure.</p>
<p>We added observability.</p>
<p>We discussed production realities.</p>
<p>And finally, we reflected on when not to adopt complexity at all.</p>
<p>That is the real evolution.</p>
<p>Not from monolith to microservices.</p>
<p>But from assumptions to understanding.</p>
<h3>Final Thought</h3>
<p>If I had to summarize everything in one idea, it would be this:</p>
<p>A great system is not one that avoids failure.</p>
<p>A great system is one that understands failure, contains it, observes it, and recovers from it quickly.</p>
<p>If microservices help you achieve that, use them.</p>
<p>If they do not, you are not obligated to use them.</p>
<p>Because in the end, the goal was never microservices.</p>
<p>The goal was always the same:</p>
<p>To build systems that survive production.</p>
]]></content:encoded></item><item><title><![CDATA[You Can Build Fast With AI. But Would You Survive Production?]]></title><description><![CDATA[Let me say this plainly.
Being able to build a project fast with AI does not make you a senior engineer.
It just means you can move fast.
And speed, on its own, has never been the definition of experience.
Today, you can scaffold an app in a weekend....]]></description><link>https://code-along.hashnode.dev/you-can-build-fast-with-ai-but-would-you-survive-production</link><guid isPermaLink="true">https://code-along.hashnode.dev/you-can-build-fast-with-ai-but-would-you-survive-production</guid><category><![CDATA[Software Engineering]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[engineering leadership]]></category><category><![CDATA[System Design]]></category><category><![CDATA[senior-software-engineer]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Tue, 27 Jan 2026 13:04:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769511900765/f462929a-dc99-4455-baf9-8f89d3fb75cd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let me say this plainly.</p>
<p>Being able to build a project fast with AI does not make you a senior engineer.</p>
<p>It just means you can move fast.</p>
<p>And speed, on its own, has never been the definition of experience.</p>
<p>Today, you can scaffold an app in a weekend.<br />Endpoints, database, auth, even tests.<br />AI will help you get there.</p>
<p>But real engineering does not show up when everything works.</p>
<p>It shows up when things start acting weird.</p>
<h3 id="heading-engineering-is-more-than-writing-code">Engineering Is More Than Writing Code</h3>
<p>I recently came across a post by <strong>Gergely Orosz</strong> where he shared a screenshot from a software architecture book.<br />The page breaks engineering into craft, production, commerce, and science.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769518848362/f34d1b47-396f-4222-a0c1-2414adeeb47b.png" alt class="image--center mx-auto" /></p>
<p>That single diagram explains more about engineering maturity than most job titles ever will.</p>
<p>At the bottom is craft.<br />That’s where many of us start.<br />You build things. You ship. You enjoy the act of creation.</p>
<p>With AI, you can do this faster now. Much faster.</p>
<p>But production is different.<br />Production is when real users show up. When uptime matters. When mistakes cost money.<br />Commerce is different. That’s when the software has to justify its existence.<br />Science is different. That’s where deep understanding, patterns, and long-term thinking live.</p>
<p>Most people calling themselves “senior” are still operating in craft.<br />Speed hides that. AI amplifies it.</p>
<p>Then I noticed a reply under that post from Taha Hussein:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769518889083/6fa7ad19-3fbf-42f7-acaa-7139d6b7bebd.png" alt class="image--center mx-auto" /></p>
<p>“Engineering isn’t just code.”</p>
<p>That line should slow you down.</p>
<p>Because AI can help you write code.<br />It cannot teach you what happens when a system starts failing quietly.<br />It cannot teach you how frontend decisions ripple into backend performance.<br />It cannot teach you how DevOps trade-offs show up as outages, cost explosions, or security gaps.</p>
<p>That kind of knowledge only comes from being there. From carrying responsibility. From making decisions when there is no clear answer.</p>
<p>AI makes you faster.<br />Experience makes you careful.</p>
<p>And engineering maturity lives in knowing the difference.</p>
<h3 id="heading-when-things-work-everyone-feels-senior">When things work, everyone feels senior</h3>
<p>You ship something.<br />It works.<br />Then traffic grows.</p>
<p>Suddenly, responses are slow.<br />Some users complain. Others don’t.<br />Nothing is fully broken, but nothing feels right.</p>
<p>Now the question is not “can you build?”<br />It’s “do you know where to look?”</p>
<p>Do you know what part of the system is under pressure?<br />Do you know what usually fails first?<br />Do you know what not to touch while debugging?</p>
<p>AI will suggest fixes.<br />Experience tells you what <em>not</em> to do.</p>
<h3 id="heading-scaling-is-not-a-tutorial-problem">Scaling is not a tutorial problem</h3>
<p>Let’s talk about scale.</p>
<p>Not the shiny kind.<br />The annoying kind.</p>
<p>The kind where you can’t change things freely anymore because people depend on it.</p>
<p>Have you ever scaled a system where a small change could affect thousands of users?</p>
<p>Have you done migrations knowing that rollback might not be possible?</p>
<p>Have you dealt with a situation where half the requests succeed and the other half fail, and nobody can explain why?</p>
<p>These moments don’t care how fast you shipped v1.</p>
<p>They care how well you understand the system.</p>
<h3 id="heading-senior-engineers-think-beyond-their-own-code-they-think-system-wide">Senior engineers think beyond their own code, they think system-wide</h3>
<p>And this isn’t just backend.</p>
<p>If you change an API response, do you think about how the frontend will break?</p>
<p>Do you think about loading states, retries, weird edge cases users will actually see?</p>
<p>Do you think about what the user experiences when your “small backend change” adds two seconds to a request?</p>
<p>Senior engineers think across the system, not just their corner of it.</p>
<h3 id="heading-production-has-a-way-of-humbling-everyone">Production has a way of humbling everyone</h3>
<p>Then there’s production.</p>
<p>Real production.</p>
<p>Have you been on-call before?</p>
<p>Have you woken up to alerts and realized logs are missing, metrics look fine, but users are angry?</p>
<p>Have you pushed a bad deploy and had to fix it while everyone is watching?</p>
<p>Have you felt that quiet pressure of knowing that real money and real trust are on the line?</p>
<p>AI doesn’t feel that.</p>
<p>You do.</p>
<h3 id="heading-what-ai-accelerates-and-what-it-cannot-replace">What AI accelerates and what it cannot replace</h3>
<p>Don’t get me wrong. AI is powerful.</p>
<p>It helps you learn faster.<br />It helps you explore ideas.<br />It helps you avoid boilerplate.</p>
<p>Use it.</p>
<p>But don’t lie to yourself about what it replaces.</p>
<p>AI does not replace judgment.<br />It does not replace responsibility.<br />It does not replace experience earned from mistakes.</p>
<h3 id="heading-a-word-for-engineers-moving-fast-with-ai">A word for engineers moving fast with AI</h3>
<p>If you’re an engineer using AI and shipping fast, that’s a good thing.</p>
<p>Build more.<br />Break things.<br />Ask questions.</p>
<p>Just understand this.</p>
<p>Seniority is not about how quickly you can build when things are calm.</p>
<p>It’s about how you think when things are not.</p>
<p>And no tool, no matter how smart, can skip that part for you.</p>
<p>…</p>
<p>If this resonated with you, share it with someone who’s building fast and trying to build right.</p>
<p>I’ll be writing more in this series about AI, engineering maturity, and leadership from real experience.<br />Follow me here and on LinkedIn to keep the conversation going.</p>
]]></content:encoded></item><item><title><![CDATA[Solutions to Dirty Read Concurrency Problem]]></title><description><![CDATA[This article is part of my concurrency series where I break down common problems like race conditions, dirty reads, phantom reads, and deadlocks in simple, glocal terms.
Again, system design is not something to joke with as a backend engineer.One of ...]]></description><link>https://code-along.hashnode.dev/solutions-to-dirty-read-concurrency-problem</link><guid isPermaLink="true">https://code-along.hashnode.dev/solutions-to-dirty-read-concurrency-problem</guid><category><![CDATA[database isolation ]]></category><category><![CDATA[dirty reads ]]></category><category><![CDATA[SQL isolation levels]]></category><category><![CDATA[concurrency-control]]></category><category><![CDATA[Transaction Management]]></category><category><![CDATA[Backend Engineering]]></category><category><![CDATA[data-consistency]]></category><category><![CDATA[SystemDesignBasics]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Sat, 27 Sep 2025 21:40:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759008584481/4621ca54-b9bc-44ef-bf55-d68168f1f1b6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This article is part of my concurrency series where I break down common problems like race conditions, dirty reads, phantom reads, and deadlocks in simple, glocal terms.</em></p>
<p><strong>Again, system design is not something to joke with as a backend engineer.</strong><br />One of the subtle issues you will encounter when dealing with concurrent systems is <strong>Dirty Reads</strong>.</p>
<p>Let me explain in the simplest way possible.</p>
<h2 id="heading-a-glocal-analogy">A Glocal Analogy</h2>
<p>Imagine you’re at a <em>buka</em> (local restaurant). You ask the waiter:</p>
<blockquote>
<p>“How much is goat meat?”</p>
</blockquote>
<p>He quickly checks with the cashier and says:</p>
<blockquote>
<p>“₦1,200.”</p>
</blockquote>
<p>You start calculating your budget in your head. But before you order, another customer buys goat meat in bulk, and the cashier immediately changes the price to <strong>₦1,500</strong>.</p>
<p>You’re stuck; you just made a decision based on a price that wasn’t real anymore.</p>
<p>That’s exactly what a <strong>Dirty Read</strong> looks like in databases.</p>
<h2 id="heading-what-is-a-dirty-read">What Is a Dirty Read?</h2>
<p>A <strong>Dirty Read</strong> happens when a transaction reads data that has been modified by another transaction but not yet committed.<br />If that other transaction is later rolled back, the first one has used data that never truly existed.</p>
<p>Think of it as:</p>
<ul>
<li><p>Transaction A updates data (but hasn’t saved it yet).</p>
</li>
<li><p>Transaction B reads that uncommitted update.</p>
</li>
<li><p>Transaction A cancels its changes.</p>
</li>
<li><p>Now Transaction B has used wrong information.</p>
</li>
</ul>
<h2 id="heading-example-in-sql">Example in SQL</h2>
<p>Here’s a simplified example using a bank account:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Transaction A</span>
<span class="hljs-keyword">BEGIN</span>;
<span class="hljs-keyword">UPDATE</span> accounts <span class="hljs-keyword">SET</span> balance = balance - <span class="hljs-number">100</span> <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span>;
<span class="hljs-comment">-- (not committed yet)</span>

<span class="hljs-comment">-- Transaction B</span>
<span class="hljs-keyword">BEGIN</span>;
<span class="hljs-keyword">SELECT</span> balance <span class="hljs-keyword">FROM</span> accounts <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span>;
<span class="hljs-comment">-- Reads the deducted balance (dirty read)</span>

<span class="hljs-comment">-- Transaction A decides to roll back</span>
<span class="hljs-keyword">ROLLBACK</span>;
</code></pre>
<p>Transaction B has now read a balance that doesn’t actually exist.</p>
<h2 id="heading-why-dirty-reads-are-dangerous">Why Dirty Reads Are Dangerous</h2>
<ul>
<li><p><strong>Banking systems</strong>: Customers see money deducted that later reappears.</p>
</li>
<li><p><strong>Inventory systems</strong>: You think stock is reduced, but it’s not.</p>
</li>
<li><p><strong>Analytics dashboards</strong>: Wrong numbers get logged, leading to bad decisions.</p>
</li>
</ul>
<p>In short: your users are interacting with <strong>lies</strong>.</p>
<p><strong>A Real-World Anecdote from My Work</strong><br />When I was building <strong>a currency exchange platform</strong>, we faced this exact problem.</p>
<p>Some users would see their wallet balance deducted after initiating a payout. But if the payout failed due to a downstream issue (like a payment provider timing out), the transaction was rolled back. The balance was restored in the database, but the user had already seen the deducted balance during that short window.</p>
<p>From the system’s point of view, nothing was “lost.” But from the user’s perspective, money had mysteriously disappeared and reappeared. That moment of confusion was enough to cause panic and support tickets.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758974406810/2ba2c3f3-d06d-4fea-ac2e-2dd55cb131e3.png" alt class="image--center mx-auto" /></p>
<p>Transaction B has trusted false data.</p>
<h2 id="heading-how-to-fix-dirty-reads">How to Fix Dirty Reads</h2>
<p>To avoid dirty reads, you need to control how transactions <strong>isolate</strong> themselves from one another.</p>
<ol>
<li><p><strong>Use READ COMMITTED isolation level</strong></p>
<ul>
<li><p>Most databases (Postgres, SQL Server, Oracle) default to this.</p>
</li>
<li><p>Ensures only committed changes can be read.</p>
</li>
<li><p>In the story I shared earlier, this adjustment stopped users from seeing balances before they were real.</p>
</li>
</ul>
</li>
<li><p><strong>Explicit locking</strong></p>
<pre><code class="lang-sql"> <span class="hljs-keyword">SELECT</span> balance 
 <span class="hljs-keyword">FROM</span> accounts 
 <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span> 
 <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">UPDATE</span>;
</code></pre>
<ul>
<li>Forces other transactions to wait until the first one is done.</li>
</ul>
</li>
<li><p><strong>Trade-offs</strong></p>
<ul>
<li><p>Stronger isolation = more accuracy.</p>
</li>
<li><p>But it can also reduce performance if not managed well.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-wrap-up">Wrap-up</h2>
<p>Just like you wouldn’t want to budget your <em>buka</em> meal on a wrong price, your systems shouldn’t run on uncommitted data.</p>
<p>Dirty Reads may sound harmless at first, but in production systems (banking, ecommerce, analytics), they can quietly destroy trust and correctness.</p>
<p>As a backend engineer, always check your database’s isolation level and ensure you’re not letting your users interact with lies.</p>
<h2 id="heading-stay-updated-amp-connected">Stay Updated &amp; Connected</h2>
<p>I’m writing a series that breaks down common <strong>concurrency problems</strong> (Race Condition, Dirty Reads, Non-repeatable Reads, Phantom Reads, Deadlocks, and more) in simple language with relatable glocal examples.</p>
<ul>
<li><p>Follow me here for new articles in the series.</p>
</li>
<li><p>Connect with me on <a target="_blank" href="https://linkedin.com/in/devfresher">LinkedIn</a> if you love conversations around backend engineering, system design, and real-world problem solving.</p>
</li>
<li><p>Share this with another engineer who might be struggling with concurrency issues.</p>
</li>
</ul>
<p>Together, let’s make backend engineering less confusing and more practical 🚀.</p>
<p>Have you ever encountered a bug caused by dirty reads without realizing it? I’d love to hear your story in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Notification System in NestJS - Part 2]]></title><description><![CDATA[In Part 1 of this series, we built the foundation of an event-driven notification system in NestJS. Events are emitted, jobs are queued with BullMQ, and workers process them in the background. That gave us a clean and fast system, where sending a not...]]></description><link>https://code-along.hashnode.dev/building-a-notification-system-in-nestjs-part-2</link><guid isPermaLink="true">https://code-along.hashnode.dev/building-a-notification-system-in-nestjs-part-2</guid><category><![CDATA[nestjs]]></category><category><![CDATA[bullmq]]></category><category><![CDATA[Redis]]></category><category><![CDATA[StrategyPattern]]></category><category><![CDATA[notifications]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Backend Development]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[System Design]]></category><category><![CDATA[System Architecture]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Thu, 25 Sep 2025 18:31:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758825007151/de289dcd-fe4b-45d5-a643-0b7fbc887028.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In <a target="_blank" href="https://code-along.hashnode.dev/building-a-notification-system-in-nestjs-part-1-the-event-driven-core">Part 1</a> of this series, we built the foundation of an event-driven notification system in NestJS. Events are emitted, jobs are queued with BullMQ, and workers process them in the background. That gave us a clean and fast system, where sending a notification never blocks the main app flow.</p>
<p>But let’s be real, notifications don’t come in just one flavor. You might send:</p>
<ul>
<li><p><strong>Email</strong> for official stuff like password resets</p>
</li>
<li><p><strong>Push</strong> alerts to keep users active on mobile</p>
</li>
<li><p><strong>In-App</strong> messages for things users should always see inside the app</p>
</li>
</ul>
<p>If we tried to handle all these directly in one service or processor, the code would quickly become spaghetti. This is where the <strong>Strategy Pattern</strong> comes in.</p>
<h2 id="heading-why-strategy">Why Strategy?</h2>
<p>Think of a strategy as a plug. Each channel (Email, Push, In-App) has its own plug, and the main notification service doesn’t care which one you use. It just picks the right plug and lets it do the work.</p>
<p>This makes it easy to:</p>
<ul>
<li><p>Add new channels later without breaking existing ones</p>
</li>
<li><p>Swap providers (e.g., Resend → SMTP for email) without rewriting logic</p>
</li>
<li><p>Keep the code neat and maintainable</p>
</li>
</ul>
<h3 id="heading-avoiding-ifelse-hell">Avoiding If/Else Hell</h3>
<p>Before Strategy, your code would look something like this:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// notification.processor.ts (bad way)</span>
<span class="hljs-keyword">async</span> process(job: Job&lt;<span class="hljs-built_in">any</span>&gt;): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
  <span class="hljs-keyword">const</span> { channel, payload } = job.data;

  <span class="hljs-keyword">if</span> (channel === <span class="hljs-string">'email'</span>) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.emailProvider.send(payload);
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (channel === <span class="hljs-string">'push'</span>) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.pushProvider.send(payload);
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (channel === <span class="hljs-string">'in-app'</span>) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.inAppRepository.save(payload);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Unsupported channel: <span class="hljs-subst">${channel}</span>`</span>);
  }
}
</code></pre>
<p><strong>Problems with this approach:</strong></p>
<ul>
<li><p>Every new channel = edit this file again.</p>
</li>
<li><p>Swapping a provider = same problem.</p>
</li>
<li><p>File grows into a giant mess.</p>
</li>
<li><p>Hard to test and maintain.</p>
</li>
</ul>
<p>Now compare with the Strategy-based approach:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// notification.processor.ts (good way)</span>
<span class="hljs-keyword">async</span> process(job: Job&lt;INotificationJobPayload&gt;): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
  <span class="hljs-keyword">const</span> { channel, payload } = job.data;
  <span class="hljs-keyword">const</span> strategy = <span class="hljs-built_in">this</span>.registry.get(channel);

  <span class="hljs-keyword">if</span> (!strategy) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnsupportedChannelException(channel);
  }

  <span class="hljs-keyword">await</span> strategy.send(payload);
}
</code></pre>
<p><strong>Benefits:</strong></p>
<ul>
<li><p>Adding new channels = just add a new strategy class.</p>
</li>
<li><p>Swapping providers = only touch that strategy.</p>
</li>
<li><p>Processor stays clean and future-proof.</p>
</li>
<li><p>Easy to test strategies in isolation.</p>
</li>
</ul>
<p>🔑 <strong>Takeaway:</strong> The Strategy Pattern prevents “switch/if/else hell” and gives you proper <strong>separation of concerns</strong>.</p>
<h2 id="heading-tools-used">Tools Used</h2>
<p>Just like in Part 1, here’s the stack behind this part:</p>
<ul>
<li><p><strong>NestJS</strong> – framework for structure</p>
</li>
<li><p><strong>BullMQ</strong> – job queue for async processing</p>
</li>
<li><p><strong>Redis</strong> – job storage and retries</p>
</li>
<li><p><strong>Strategy Pattern</strong> – cleanly separating notification channels</p>
</li>
</ul>
<h2 id="heading-setting-up-the-channel-strategies">Setting Up the Channel Strategies</h2>
<p>First, let’s define an enum for our notification channels:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// enums/notification-channel.enum.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-built_in">enum</span> NotificationChannel {
  EMAIL = <span class="hljs-string">'email'</span>,
  PUSH = <span class="hljs-string">'push'</span>,
  IN_APP = <span class="hljs-string">'in-app'</span>,
  SMS = <span class="hljs-string">'sms'</span>,
  WHATSAPP = <span class="hljs-string">'whatsapp'</span>
}
</code></pre>
<p>Now, define a simple interface that all channel strategies will follow:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// interfaces/notification-strategy.interface.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> INotificationStrategy&lt;TPayload = any&gt; {
  send(payload: TPayload): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
}
</code></pre>
<p>Define sample interfaces for each of the channels payload:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// strategies/email/interfaces/email-payload.interface.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> IEmailPayload {
  to: <span class="hljs-built_in">string</span>;
  <span class="hljs-keyword">from</span>?: <span class="hljs-built_in">string</span>;
  subject: <span class="hljs-built_in">string</span>;
  templateName?: <span class="hljs-built_in">string</span>;
  body?: <span class="hljs-built_in">string</span>;
  replacements?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
}

<span class="hljs-comment">// strategies/in-app/interfaces/in-app-payload.interface.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> IInAppPayload {
  recipientId: <span class="hljs-built_in">string</span>;
  title: <span class="hljs-built_in">string</span>;
  message: <span class="hljs-built_in">string</span>;
  metadata?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
}

<span class="hljs-comment">// strategies/push/interfaces/push-payload.interface.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> IPushPayload {
  recipientTokens: <span class="hljs-built_in">string</span>[];
  recipientId: <span class="hljs-built_in">string</span>;
  title: <span class="hljs-built_in">string</span>;
  message: <span class="hljs-built_in">string</span>;
  data?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;;
}
</code></pre>
<p>Then create separate strategies. Example:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// strategies/email/email.strategy.ts</span>
<span class="hljs-keyword">import</span> { Injectable, InternalServerErrorException, Logger } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { INotificationStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../interfaces/notification-strategy.interface'</span>;
<span class="hljs-keyword">import</span> { IEmailPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./interfaces/email-payload.interface'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> EmailNotificationStrategy
  <span class="hljs-keyword">implements</span> INotificationStrategy&lt;IEmailPayload&gt;
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger = <span class="hljs-keyword">new</span> Logger(EmailNotificationStrategy.name);

  <span class="hljs-keyword">async</span> send(payload: IEmailPayload): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-comment">// logic for sending email (Resend, SMTP, etc.)</span>
    <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Sending EMAIL to <span class="hljs-subst">${payload.to}</span>: <span class="hljs-subst">${payload.subject}</span>`</span>);
  }
}
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// strategies/push/push.strategy.ts</span>
<span class="hljs-keyword">import</span> { Injectable, InternalServerErrorException, Logger } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { INotificationStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../interfaces/notification-strategy.interface'</span>;
<span class="hljs-keyword">import</span> { IPushPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./interfaces/push-payload.interface'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PushNotificationStrategy
  <span class="hljs-keyword">implements</span> INotificationStrategy&lt;IPushPayload&gt;
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger = <span class="hljs-keyword">new</span> Logger(PushNotificationStrategy.name);

  <span class="hljs-keyword">async</span> send(payload: IPushPayload) {
    <span class="hljs-comment">// logic for FCM or OneSignal</span>
    <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Sending PUSH to <span class="hljs-subst">${payload.recipientTokens.length}</span> devices: <span class="hljs-subst">${payload.title}</span>`</span>);
  }
}
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// strategies/in-app/in-app.strategy.ts</span>
<span class="hljs-keyword">import</span> { Injectable, InternalServerErrorException, Logger } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { INotificationStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../interfaces/notification-strategy.interface'</span>;
<span class="hljs-keyword">import</span> { IInAppPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./interfaces/in-app-payload.interface'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> InAppNotificationStrategy
  <span class="hljs-keyword">implements</span> INotificationStrategy&lt;IInAppPayload&gt;
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger = <span class="hljs-keyword">new</span> Logger(InAppNotificationStrategy.name);

  <span class="hljs-keyword">async</span> send(payload: IInAppPayload) {
    <span class="hljs-comment">// save in DB as in-app notification</span>
    <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Saving IN-APP notification for <span class="hljs-subst">${payload.recipientId}</span>`</span>);
  }
}
</code></pre>
<h2 id="heading-strategy-registry">Strategy Registry</h2>
<p>We need a place to register and retrieve the right strategy based on the channel:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// notification-strategy-registry.ts</span>
<span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { NotificationChannel } <span class="hljs-keyword">from</span> <span class="hljs-string">'../enums/notification-channel.enum'</span>;
<span class="hljs-keyword">import</span> { INotificationStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'./interfaces/notification-strategy.interface'</span>;
<span class="hljs-keyword">import</span> { EmailNotificationStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/email/email.strategy'</span>;
<span class="hljs-keyword">import</span> { PushNotificationStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/push/push.strategy'</span>;
<span class="hljs-keyword">import</span> { InAppNotificationStrategy } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/in-app/in-app.strategy'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NotificationStrategyRegistry {
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> strategies = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;NotificationChannel, INotificationStrategy&gt;();

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    email: EmailNotificationStrategy,
    push: PushNotificationStrategy,
    inApp: InAppNotificationStrategy,
    sms: SmsNotificationStrategy,
  </span>) {
    <span class="hljs-built_in">this</span>.strategies.set(NotificationChannel.EMAIL, email);
    <span class="hljs-built_in">this</span>.strategies.set(NotificationChannel.PUSH, push);
    <span class="hljs-built_in">this</span>.strategies.set(NotificationChannel.IN_APP, inApp);
  }

  get(channel: NotificationChannel): INotificationStrategy | <span class="hljs-literal">undefined</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.strategies.get(channel);
  }
}
</code></pre>
<h2 id="heading-putting-it-together-in-the-workerprocessor">Putting It Together in the Worker/Processor</h2>
<p>Now the <code>NotificationProcessor</code> can use the registry to dispatch without caring about the internals. Before that, let’s define an interface that unifies the payloads of all the channels.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// interfaces/notification-job.interface.ts</span>
<span class="hljs-keyword">import</span> { NotificationChannel } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/modules/notification/enums/notification-channel.enum'</span>;
<span class="hljs-keyword">import</span> { IEmailPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'../strategies/email/interfaces/email-payload.interface'</span>;
<span class="hljs-keyword">import</span> { IPushPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'../strategies/push/interfaces/push-payload.interface'</span>;
<span class="hljs-keyword">import</span> { IInAppPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'../strategies/in-app/interfaces/in-app-payload.interface'</span>;
<span class="hljs-keyword">import</span> { ISmsPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'../strategies/sms/interfaces/sms-payload.interface'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> INotificationJobPayload =
  | { channel: NotificationChannel.EMAIL; payload: IEmailPayload }
  | { channel: NotificationChannel.PUSH; payload: IPushPayload&gt; }
  | { channel: NotificationChannel.IN_APP; payload: IInAppPayload }
  | { channel: NotificationChannel.SMS; payload: ISmsPayload };
</code></pre>
<p>Now we can use that in the <code>NotificationProcessor</code> :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// notification.processor.ts</span>
<span class="hljs-keyword">import</span> { OnWorkerEvent, Processor, WorkerHost } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/bullmq'</span>;
<span class="hljs-keyword">import</span> { Logger } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { Job } <span class="hljs-keyword">from</span> <span class="hljs-string">'bullmq'</span>;

<span class="hljs-meta">@Processor</span>(<span class="hljs-string">'notification-queue'</span>)
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NotificationProcessor <span class="hljs-keyword">extends</span> WorkerHost {
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger = <span class="hljs-keyword">new</span> Logger(NotificationProcessor.name);

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> registry: NotificationStrategyRegistry</span>) {
    <span class="hljs-built_in">super</span>();
  }

  <span class="hljs-meta">@OnWorkerEvent</span>(<span class="hljs-string">'active'</span>)
  onActive(job: Job&lt;INotificationJobPayload&gt;) {
    <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Processing job <span class="hljs-subst">${job.name}</span> with id <span class="hljs-subst">${job.id}</span>`</span>);
  }

  <span class="hljs-keyword">async</span> process(job: Job&lt;INotificationJobPayload&gt;): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">const</span> { payload, channel } = job.data;
    <span class="hljs-keyword">const</span> strategy = <span class="hljs-built_in">this</span>.registry.get(channel);

    <span class="hljs-keyword">if</span> (!strategy) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> BadRequestException(
        <span class="hljs-string">`Unsupported notification channel <span class="hljs-subst">${channel}</span>`</span>,
      );
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> strategy.send(payload);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">this</span>.logger.error(<span class="hljs-string">`Failed to send via <span class="hljs-subst">${channel}</span>: <span class="hljs-subst">${err.message}</span>`</span>);
    }
  }
}
</code></pre>
<h2 id="heading-expose-interactable-api-in-the-service">Expose Interactable API in the Service</h2>
<p>Now in the service layer, we can expose methods that enqueues the notification as a job that a callable from the event listeners.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { InjectQueue } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/bullmq'</span>;
<span class="hljs-keyword">import</span> { Queue } <span class="hljs-keyword">from</span> <span class="hljs-string">'bullmq'</span>;
<span class="hljs-keyword">import</span> { NotificationChannel } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/modules/notification/enums/notification-channel.enum'</span>;
<span class="hljs-keyword">import</span> { IEmailPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/email/interfaces/email-payload.interface'</span>;
<span class="hljs-keyword">import</span> { IPushPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/push/interfaces/push-payload.interface'</span>;
<span class="hljs-keyword">import</span> { IInAppPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/in-app/interfaces/in-app-payload.interface'</span>;
<span class="hljs-keyword">import</span> { INotificationJobPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./interfaces/notification-job.interface'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NotificationService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-meta">@InjectQueue</span>(<span class="hljs-string">'notification-queue'</span>) <span class="hljs-keyword">private</span> notificationQueue: Queue,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> userDeviceService: UserDeviceService,
  </span>) {}

  <span class="hljs-keyword">async</span> notifyEmail(payload: IEmailPayload) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.enqueue({ channel: NotificationChannel.EMAIL, payload });
  }

  <span class="hljs-keyword">async</span> notifyPush(payload: IPushPayload) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.enqueue({ channel: NotificationChannel.PUSH, payload });
  }

  <span class="hljs-keyword">async</span> notifyInApp(payload: IInAppPayload) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.enqueue({ channel: NotificationChannel.IN_APP, payload });
  }

  <span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> enqueue(jobPayload: INotificationJobPayload) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.notificationQueue.add(<span class="hljs-string">'dispatch'</span>, jobPayload, {
      attempts: <span class="hljs-number">3</span>,
      backoff: { <span class="hljs-keyword">type</span>: <span class="hljs-string">'exponential'</span>, delay: <span class="hljs-number">5000</span> },
      removeOnComplete: <span class="hljs-literal">true</span>,
      removeOnFail: <span class="hljs-literal">false</span>,
    });
  }
}
</code></pre>
<p>The event listeners can then easily call:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.notificationService.notifyEmail({
  to: payload.email,
  subject: <span class="hljs-string">'Welcome to our platform'</span>,
  templateName: <span class="hljs-string">'welcome'</span>,
  replacements: {
    name: payload.firstName
  },
});

<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.notificationService.notifyPush({
  recipientId: user.id,
  title: <span class="hljs-string">'Wallet Credited'</span>,
  message: <span class="hljs-string">`<span class="hljs-subst">${formattedAmount}</span> credited. Balance: <span class="hljs-subst">${formattedNewBalance}</span>.`</span>,
});
</code></pre>
<p>We can as well create a sugar method <code>notifyAll</code>, that uses all the notification channels concurrently. Before we define that, let’s define what the payload for that will look like. This will keep our code strongly typed and easy to extend later.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// intefaces/notification-payloads.ts</span>
<span class="hljs-keyword">import</span> { NotificationChannel } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/modules/notification/enums/notification-channel.enum'</span>;
<span class="hljs-keyword">import</span> { IEmailPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'../strategies/email/interfaces/email-payload.interface'</span>;
<span class="hljs-keyword">import</span> { IPushPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'../strategies/push/interfaces/push-payload.interface'</span>;
<span class="hljs-keyword">import</span> { IInAppPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'../strategies/in-app/interfaces/in-app-payload.interface'</span>;
<span class="hljs-keyword">import</span> { ISmsPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'../strategies/sms/interfaces/sms-payload.interface'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> NotificationPayloads = {
  [NotificationChannel.EMAIL]: IEmailPayload;
  [NotificationChannel.PUSH]: IPushPayload&gt;;
  [NotificationChannel.IN_APP]: IInAppPayload;
  [NotificationChannel.SMS]: ISmsPayload;
};
</code></pre>
<p>Finally update the <code>NotificationService</code> with <code>notifyAll</code> sugar.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { InjectQueue } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/bullmq'</span>;
<span class="hljs-keyword">import</span> { Queue } <span class="hljs-keyword">from</span> <span class="hljs-string">'bullmq'</span>;
<span class="hljs-keyword">import</span> { NotificationChannel } <span class="hljs-keyword">from</span> <span class="hljs-string">'src/modules/notification/enums/notification-channel.enum'</span>;
<span class="hljs-keyword">import</span> { IEmailPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/email/interfaces/email-payload.interface'</span>;
<span class="hljs-keyword">import</span> { IPushPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/push/interfaces/push-payload.interface'</span>;
<span class="hljs-keyword">import</span> { IInAppPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./strategies/in-app/interfaces/in-app-payload.interface'</span>;
<span class="hljs-keyword">import</span> { INotificationJobPayload } <span class="hljs-keyword">from</span> <span class="hljs-string">'./interfaces/notification-job.interface'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NotificationService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-meta">@InjectQueue</span>(<span class="hljs-string">'notification-queue'</span>) <span class="hljs-keyword">private</span> notificationQueue: Queue,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> userDeviceService: UserDeviceService,
  </span>) {}

  <span class="hljs-keyword">async</span> notifyAll(payloads: Partial&lt;NotificationPayloads&gt;) {
    <span class="hljs-keyword">const</span> jobs: <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;[] = [];

    <span class="hljs-keyword">if</span> (payloads.email)
      jobs.push(<span class="hljs-built_in">this</span>.notifyEmail(payloads[NotificationChannel.EMAIL]));
    <span class="hljs-keyword">if</span> (payloads.push)
      jobs.push(<span class="hljs-built_in">this</span>.notifyPush(payloads[NotificationChannel.PUSH]));
    <span class="hljs-keyword">if</span> (payloads[<span class="hljs-string">'in-app'</span>])
      jobs.push(<span class="hljs-built_in">this</span>.notifyInApp(payloads[NotificationChannel.IN_APP]));

    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(jobs);
  }

  <span class="hljs-comment">// ... existing methods</span>
}
</code></pre>
<p>Then, you can call it in the event listeners like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.notificationService.notifyAll({
  [NotificationChannel.EMAIL]: {
    to: user.email,
    subject: <span class="hljs-string">'Wallet Debited'</span>,
    templateName: <span class="hljs-string">'wallet-debited'</span>,
    replacements: {
      name: user.firstName,
      transaction_date: payload.createdAt.toLocaleDateString(),
      new_balance: formattedNewBalance,
      amount: formattedAmount,
      transaction_reference: payload.reference,          },
  },
  [NotificationChannel.PUSH]: {
    recipientId: user.id,
    title: <span class="hljs-string">'Wallet Debited'</span>,
    message: <span class="hljs-string">`<span class="hljs-subst">${formattedAmount}</span> debited. Balance: <span class="hljs-subst">${formattedNewBalance}</span>.`</span>,
  },
  [NotificationChannel.IN_APP]: {
    recipientId: user.id,
    title: <span class="hljs-string">'Wallet Debited'</span>,
    message: <span class="hljs-string">`<span class="hljs-subst">${formattedAmount}</span> debited. Balance: <span class="hljs-subst">${formattedNewBalance}</span>.`</span>,
    metadata: { transactionId: transaction.id },
  },
});
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Imagine this scenario</div>
</div>

<blockquote>
<p>A user’s <strong>wallet is credited</strong>.<br />They should instantly get:</p>
<ul>
<li><p>An <strong>Email</strong> receipt with transaction details.</p>
</li>
<li><p>A <strong>Push Notification</strong> on their phone.</p>
</li>
<li><p>An <strong>In-App message</strong> when they open the app.</p>
</li>
</ul>
</blockquote>
<p>Instead of cluttering your code with <code>if/else</code> or <code>switch</code>, each <strong>channel strategy</strong> handles its own job, keeping your processor clean and extensible.</p>
<h2 id="heading-whats-next">What’s Next?</h2>
<p>We’ve now decoupled <em>event → queue → worker → channel strategy</em>.</p>
<p>But channels (Email, Push, In-App) still need actual <strong>providers</strong>:</p>
<ul>
<li><p>Email → Resend or SMTP</p>
</li>
<li><p>Push → FCM or OneSignal</p>
</li>
<li><p>SMS → Twilio</p>
</li>
</ul>
<p>In <strong>Part 3</strong>, we’ll use the <strong>Bridge Pattern</strong> to connect channels to their providers, so you can switch providers without touching your business logic.</p>
<h2 id="heading-stay-updated-amp-connected">Stay Updated &amp; Connected</h2>
<p>If you enjoyed this article and want to follow along as I continue the <strong>Notification System series</strong> and other backend deep-dives, let’s connect:</p>
<ul>
<li><p><a target="_blank" href="https://www.linkedin.com/in/usman-soliu">LinkedIn</a> — I share backend and system design insights weekly</p>
</li>
<li><p><a target="_blank" href="https://github.com/devfresher">GitHub</a> — Code samples, side projects, and experiments</p>
</li>
<li><p><a target="_blank" href="https://x.com/devfresher">X/Twitter</a> — Short takes and tech thoughts</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Building a Notification System in NestJS – Part 1]]></title><description><![CDATA[Notifications are everywhere. Whether it’s an email about a new transaction, a push alert from your favorite app, or an in-app bell icon lighting up, notifications are a big part of how apps keep users engaged.
In this series, I’ll Walk you through h...]]></description><link>https://code-along.hashnode.dev/building-a-notification-system-in-nestjs-part-1-the-event-driven-core</link><guid isPermaLink="true">https://code-along.hashnode.dev/building-a-notification-system-in-nestjs-part-1-the-event-driven-core</guid><category><![CDATA[nestjs]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Backend Development]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[bullmq]]></category><category><![CDATA[Redis]]></category><category><![CDATA[System Design]]></category><category><![CDATA[System Architecture]]></category><category><![CDATA[notifications]]></category><category><![CDATA[scalability]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Thu, 25 Sep 2025 13:32:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758825039911/d39e6f7b-6a4b-4662-a358-6ebd47a1a2da.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Notifications are everywhere. Whether it’s an email about a new transaction, a push alert from your favorite app, or an in-app bell icon lighting up, notifications are a big part of how apps keep users engaged.</p>
<p>In this series, I’ll Walk you through how I built a <strong>flexible, event-driven notification system</strong> in <strong>NestJS</strong>. The system supports multiple channels (Email, Push, and In-App), can plug into different providers (like Resend or SMTP for email, Firebase Cloud Messaging or OneSignal for push), and is built to scale.</p>
<p>Let’s start with <strong>the core idea</strong>:</p>
<h3 id="heading-why-event-driven">Why Event-Driven?</h3>
<p>Notifications are essential but building them the <strong><em>wrong way</em></strong> can make your code messy fast. Imagine creating a user account and waiting seconds because the system is busy sending a welcome email. That’s a bad experience.</p>
<p>Let’s see that in action.</p>
<h3 id="heading-the-naive-approach">The naïve approach</h3>
<p>Imagine this user registration flow:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> registerUser(dto: RegisterUserDto) {
  <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.userRepo.save(dto);

  <span class="hljs-comment">// Naïve: directly send an email here</span>
  <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.emailService.sendWelcome(user.email);

  <span class="hljs-keyword">return</span> user;
}
</code></pre>
<p>Looks simple, but now:</p>
<ul>
<li><p>Your <code>UserService</code> is tied to email logic.</p>
</li>
<li><p>If you later add push notifications or in-app alerts, you’ll clutter this service with multiple channels.</p>
</li>
<li><p>Failures in email sending can break user registration.</p>
</li>
</ul>
<p>Instead, I used <strong>event-driven architecture</strong>. With this approach:</p>
<ul>
<li><p>The main app only <strong>emits an event</strong> (like <code>user.registered</code>).</p>
</li>
<li><p>A notification service picks up the event and does the heavy lifting in the background.</p>
</li>
<li><p>Users get their notifications without slowing down the main app.</p>
</li>
</ul>
<p>This makes the system <strong>fast, clean, and decoupled</strong>.</p>
<h3 id="heading-tools-i-used">Tools I Used</h3>
<ul>
<li><p><strong>NestJS</strong> – The framework tying everything together.</p>
</li>
<li><p><strong>EventEmitter</strong> – To fire events inside the app.</p>
</li>
<li><p><strong>BullMQ</strong> – For background job processing and retries.</p>
</li>
</ul>
<h2 id="heading-setting-up-the-core">Setting Up the Core</h2>
<p>Here’s the minimal setup I used to get the event-driven flow working:</p>
<h3 id="heading-1-install-dependencies">1. Install Dependencies</h3>
<p>We need the event system (<code>@nestjs/event-emitter</code>), job queue (<code>bullmq</code>), and Redis driver (<code>ioredis</code>) to enable event-driven notifications.</p>
<pre><code class="lang-powershell">npm install @nestjs/event<span class="hljs-literal">-emitter</span> bullmq ioredis
</code></pre>
<h3 id="heading-2-enable-eventemitter">2. Enable EventEmitter</h3>
<p>This turns on the event system in NestJS, so we can emit and listen to events across the app.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app.module.ts</span>
<span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { EventEmitterModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/event-emitter'</span>;
<span class="hljs-keyword">import</span> { NotificationModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'./notification/notification.module'</span>;

<span class="hljs-meta">@Module</span>({
  imports: [
    EventEmitterModule.forRoot(), <span class="hljs-comment">// enables event system</span>
    NotificationModule,
  ],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AppModule {}
</code></pre>
<h3 id="heading-3-setup-bullmq-queue">3. Setup BullMQ Queue</h3>
<p>Here we register a queue called <code>notification-queue</code>. This is where notification jobs will be added and processed asynchronously</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// notification.module.ts</span>
<span class="hljs-keyword">import</span> { Module } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { BullModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/bullmq'</span>;
<span class="hljs-keyword">import</span> { NotificationListener } <span class="hljs-keyword">from</span> <span class="hljs-string">'./notification.listener'</span>;
<span class="hljs-keyword">import</span> { NotificationProcessor } <span class="hljs-keyword">from</span> <span class="hljs-string">'./notification.processor'</span>;

<span class="hljs-meta">@Module</span>({
  imports: [
    BullModule.registerQueue({
      name: <span class="hljs-string">'notification-queue'</span>,
    }),
  ],
  providers: [NotificationListener, NotificationProcessor],
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NotificationModule {}
</code></pre>
<h3 id="heading-4-emit-an-event">4. Emit an Event</h3>
<p>When a user registers, we emit an event (<code>user.registered</code>) instead of sending the email directly. This keeps the main flow fast and clean.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// user.service.ts</span>
<span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { EventEmitter2 } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/event-emitter'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> UserService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> eventEmitter: EventEmitter2</span>) {}

  <span class="hljs-keyword">async</span> registerUser(dto: <span class="hljs-built_in">any</span>) {
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.createUser(dto);

    <span class="hljs-comment">// fire event</span>
    <span class="hljs-built_in">this</span>.eventEmitter.emit(<span class="hljs-string">'user.registered'</span>, { userId: user.id });
    <span class="hljs-keyword">return</span> user;
  }

  <span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> createUser(dto: <span class="hljs-built_in">any</span>) {
    <span class="hljs-comment">// fake user creation</span>
    <span class="hljs-keyword">return</span> { id: <span class="hljs-string">'123'</span>, ...dto };
  }
}
</code></pre>
<h3 id="heading-5-event-listener-captures-event">5. Event Listener (captures event)</h3>
<p>Our listener picks up the <code>user.registered</code> event and hands it off to the notification service, which will queue the job.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// notification.listener.ts</span>
<span class="hljs-keyword">import</span> { OnEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/event-emitter'</span>;
<span class="hljs-keyword">import</span> { InjectQueue } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/bullmq'</span>;
<span class="hljs-keyword">import</span> { Queue } <span class="hljs-keyword">from</span> <span class="hljs-string">'bullmq'</span>;
<span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NotificationListener {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> notificationService: NotificationService</span>) {}

  <span class="hljs-meta">@OnEvent</span>(<span class="hljs-string">'user.registered'</span>)
  <span class="hljs-keyword">async</span> handleUserRegistered(payload: User) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.notificationService.notifyEmail({
      to: payload.email,
      subject: <span class="hljs-string">'Welcome to our platform'</span>,
      templateName: <span class="hljs-string">'welcome'</span>,
      replacements: {
        name: payload.firstName
      },
    });
  }
}
</code></pre>
<h3 id="heading-6-service-adds-job-to-queue">6. Service (adds job to queue)</h3>
<p>We define what an email payload looks like, then enqueue it with BullMQ. Retry and backoff options ensure reliability.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// email-payload.interface.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> IEmailPayload {
  to: <span class="hljs-built_in">string</span>;
  <span class="hljs-keyword">from</span>?: <span class="hljs-built_in">string</span>;
  subject: <span class="hljs-built_in">string</span>;
  templateName?: <span class="hljs-built_in">string</span>;
  body?: <span class="hljs-built_in">string</span>;
  replacements?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
}


<span class="hljs-comment">// notification.service.ts</span>
<span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { InjectQueue } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/bullmq'</span>;
<span class="hljs-keyword">import</span> { Queue } <span class="hljs-keyword">from</span> <span class="hljs-string">'bullmq'</span>;

<span class="hljs-meta">@Injectable</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NotificationService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-meta">@InjectQueue</span>(<span class="hljs-string">'notification-queue'</span>) <span class="hljs-keyword">private</span> notificationQueue: Queue,
  </span>) {}

  <span class="hljs-keyword">async</span> notifyEmail(payload: IEmailPayload) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.notificationQueue.add(<span class="hljs-string">'dispatch'</span>, jobPayload, {
      attempts: <span class="hljs-number">3</span>,
      backoff: { <span class="hljs-keyword">type</span>: <span class="hljs-string">'exponential'</span>, delay: <span class="hljs-number">5000</span> },
      removeOnComplete: <span class="hljs-literal">true</span>,
      removeOnFail: <span class="hljs-literal">false</span>,
    });;
  }
}
</code></pre>
<h3 id="heading-7-process-jobs">7. Process Jobs</h3>
<p>The processor actually does the sending. In this foundation, we just log, but later we’ll plug in real providers (SMTP, Resend, FCM, OneSignal, etc.).</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// notification.processor.ts</span>
<span class="hljs-keyword">import</span> { OnWorkerEvent, Processor, WorkerHost } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/bullmq'</span>;
<span class="hljs-keyword">import</span> { Logger } <span class="hljs-keyword">from</span> <span class="hljs-string">'@nestjs/common'</span>;
<span class="hljs-keyword">import</span> { Job } <span class="hljs-keyword">from</span> <span class="hljs-string">'bullmq'</span>;

<span class="hljs-meta">@Processor</span>(<span class="hljs-string">'notification-queue'</span>)
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> NotificationProcessor <span class="hljs-keyword">extends</span> WorkerHost {
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> logger = <span class="hljs-keyword">new</span> Logger(NotificationProcessor.name);

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-built_in">super</span>();
  }

  <span class="hljs-meta">@OnWorkerEvent</span>(<span class="hljs-string">'active'</span>)
  onActive(job: Job&lt;IEmailPayload&gt;) {
    <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Processing job <span class="hljs-subst">${job.name}</span> with id <span class="hljs-subst">${job.id}</span>`</span>);
  }

  <span class="hljs-keyword">async</span> process(job: Job&lt;IEmailPayload&gt;): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
      <span class="hljs-built_in">this</span>.logger.log(<span class="hljs-string">`Sending email to user <span class="hljs-subst">${job.data.to}</span>`</span>);
  }
}
</code></pre>
<p>So, what just happened?</p>
<ol>
<li><p>Zayd registers with <a target="_blank" href="mailto:john@email.com"><code>zayd@email.com</code></a>.</p>
</li>
<li><p><code>UserService</code> saves him and emits <code>user.registered</code>.</p>
</li>
<li><p>Notification module listens for that event.</p>
</li>
<li><p>The listener, <code>NotificationListener</code> passes it to a service that adds a job to BullMQ <code>notification-queue</code>.</p>
</li>
<li><p>The job worker, <code>NotificationProcessor</code> picks it up and logs:</p>
</li>
</ol>
<pre><code class="lang-css">📧 <span class="hljs-selector-tag">Sending</span> <span class="hljs-selector-tag">welcome</span> <span class="hljs-selector-tag">email</span> <span class="hljs-selector-tag">to</span> <span class="hljs-selector-tag">user</span> <span class="hljs-selector-tag">zayd</span><span class="hljs-keyword">@email</span>.com
</code></pre>
<ol start="6">
<li>Zayd gets his notification, and your business logic stays clean. 🎉</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758742681924/11f84c32-b7c2-480a-ac8e-3bf85cf6c4c3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-wrap-up">Wrap up</h2>
<p>That’s the foundation. We now have the <strong>skeleton of an event-driven notification system</strong>:</p>
<ul>
<li><p>Clean separation of concerns</p>
</li>
<li><p>Async and fault-tolerant with queues</p>
</li>
<li><p>Ready for multiple channels</p>
</li>
</ul>
<p>In the <a target="_blank" href="https://code-along.hashnode.dev/building-a-notification-system-in-nestjs-part-2-the-smart-strategy-pattern-cmfzr27st000202ky2573g6y8">next part</a>, I’ll explain how I used the <strong>strategy pattern</strong> to handle different notification channels (Email, Push, In-App) without making a mess in the codebase.</p>
<h2 id="heading-stay-updated-amp-connected">Stay Updated &amp; Connected</h2>
<p>If you enjoyed this article and want to follow along as I continue the <strong>Notification System series</strong> and other backend deep-dives, let’s connect:</p>
<ul>
<li><p><a target="_blank" href="https://www.linkedin.com/in/usman-soliu">LinkedIn</a> — I share backend and system design insights weekly</p>
</li>
<li><p><a target="_blank" href="https://github.com/devfresher">GitHub</a> — Code samples, side projects, and experiments</p>
</li>
<li><p><a target="_blank" href="https://x.com/devfresher">X/Twitter</a> — Short takes and tech thoughts</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The Buka Series [Part 2]]]></title><description><![CDATA[You already know what HTTP is now, right?
No? 👉 Check out this post first HTTP beyond the acronym and come back here.
Now, what about the extra S in HTTPS?
Most people just say, “It stands for Secure,” and move on.But let’s really understand what ma...]]></description><link>https://code-along.hashnode.dev/the-buka-series-part-2</link><guid isPermaLink="true">https://code-along.hashnode.dev/the-buka-series-part-2</guid><category><![CDATA[https]]></category><category><![CDATA[http]]></category><category><![CDATA[backend]]></category><category><![CDATA[System Design]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Fri, 11 Apr 2025 11:20:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744370276398/88bfd060-5c40-431e-af11-224775f566e0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>You already know what HTTP is now, right?</strong></p>
<p>No? 👉 Check out this post first <a target="_blank" href="https://hashnode.com/post/cm9cm2u8m000q09lj07n4ex0e">HTTP beyond the acronym</a> and come back here.</p>
<p>Now, what about the extra <strong>S</strong> in <strong>HTTPS</strong>?</p>
<p>Most people just say, “It stands for <em>Secure</em>,” and move on.<br />But let’s really understand what makes it secure.</p>
<p>Let’s go back to our favorite analogy:</p>
<p><strong>Remember HTTP?  
</strong>That’s your trusted <strong>amala</strong> (<em>a Nigerian delicacy</em>) delivery guy. He moves between you (the <strong>client</strong>) and your favorite <strong>buka</strong> (<em>a local food spot or street restaurant</em>) — basically, your server.</p>
<p>Now here's where it gets interesting.</p>
<p>With <strong>HTTP</strong>, your guy walks there casually, says your order out loud, picks it up, and walks back.</p>
<p>It works fine, but here’s the risk:</p>
<p>👀 Anyone watching can:</p>
<ul>
<li><p>Hear your order</p>
</li>
<li><p>Tamper with it</p>
</li>
<li><p>Or pretend to be you and make a fake request</p>
</li>
</ul>
<p>Now picture <strong>HTTPS</strong>.</p>
<p>You’re still using the same delivery guy.<br />But this time, he’s dressed smartly, carrying a <strong>secure briefcase</strong>, and your message is locked inside.</p>
<p>Only the buka has the right key to open it.</p>
<p>Even if someone intercepts him on the way, all they see is scrambled nonsense like:<br /><strong>“7dfj23&amp;^%$#kld==”</strong> instead of “<strong>GET</strong> me amala and abula.”</p>
<p>Your request stays protected from the moment it leaves you to the moment it reaches the buka. The same protection applies to the response coming back.</p>
<p>That’s the big difference:</p>
<p>✅ <strong>HTTP</strong> sends plain text that’s easy to read and steal<br />✅ <strong>HTTPS</strong> scrambles the message so only the right server can understand</p>
<p>This is why HTTPS is very important for:<br />🔐 Login pages<br />💳 Online payments<br />🧾 Any sensitive data transfer</p>
<p>It’s not just an <strong>S</strong>.<br />It’s your shield. It’s your padlock. It’s what keeps your information safe.</p>
<p>So next time someone asks:<br /><strong>“What’s the difference between HTTP and HTTPS?”</strong></p>
<p>Tell them:<br /><strong>“One delivers openly. The other protects your message like a top-secret mission.”</strong></p>
<p>See you in the next!</p>
]]></content:encoded></item><item><title><![CDATA[The Buka Series [Part 1]]]></title><description><![CDATA[When you're asked in a technical interview to explain what HTTP is...
After you mention it's a protocol (HyperText Transfer Protocol) used to transfer data over the web...
Please, don’t stop there. That’s just the abbreviation.
What you really want t...]]></description><link>https://code-along.hashnode.dev/the-buka-series-part-1</link><guid isPermaLink="true">https://code-along.hashnode.dev/the-buka-series-part-1</guid><category><![CDATA[http]]></category><category><![CDATA[backend]]></category><category><![CDATA[System Design]]></category><category><![CDATA[interview]]></category><category><![CDATA[Technical interview]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Fri, 11 Apr 2025 09:55:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744365071745/1d1712a6-ff7a-4fca-97ca-b73d4aa5c57a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you're asked in a technical interview to explain what HTTP is...</p>
<p>After you mention it's a protocol (HyperText Transfer Protocol) used to transfer data over the web...</p>
<p><strong>Please, don’t stop there. That’s just the abbreviation.</strong></p>
<p>What you really want to explain is how HTTP works and why it matters in real-world applications.</p>
<p>Now, use this simple analogy to understand that dude once and for all:</p>
<p><strong>Think of HTTP like sending someone to get amala from your favorite buka.</strong></p>
<p>You (the <strong>client</strong>) are hungry.<br />The <strong><em>buka</em></strong> (a local food spot or street restaurant) which is regarded as the <strong>server</strong> has what you want.<br />But you’re not going yourself, you’re sending a trusted guy (HTTP) to go there, collect it, and bring it back.</p>
<p>Here’s how it plays out:</p>
<ul>
<li><p>You tell him what you want: “<strong>GET</strong> me <em>amala</em>, <em>abula</em> (a Nigerian soup) and <em>ogunfe</em> (goat meat) 🤤.”</p>
</li>
<li><p>Or you say: “<strong>POST</strong> this new meal idea — mixed rice and beans with palm oil stew — into their suggestion box.”</p>
</li>
<li><p>You could even say: “<strong>PUT</strong> this correction in my usual order.” or “<strong>DELETE</strong> my order entirely.”</p>
</li>
</ul>
<p>When he gets back, he gives you a report:</p>
<ul>
<li><p><code>200</code> — All went well. Here’s your food.</p>
</li>
<li><p><code>404</code> — Buka no get <em>amala</em> today.</p>
</li>
<li><p><code>500</code> — Kitchen catch fire. Everything scatter.</p>
</li>
</ul>
<p>He also gives you some notes (<strong>headers</strong>):</p>
<ul>
<li><p>“The <em>buka</em> says next time come early o.”</p>
</li>
<li><p>“They served it hot 🔥, and they added <em>ponmo</em> (cow skin) for free.”</p>
</li>
</ul>
<p>And the actual food itself? That’s the <strong>body</strong> of the response. That’s what you really needed.</p>
<p>But here’s the funny thing, the guy forgets you after every trip 😭.<br />Unless you hand him a token or cookie like “This is from me, Usman. They know me there.”</p>
<p>That’s HTTP: stateless, structured, fast — and when upgraded (HTTP/2, HTTP/3), he now moves faster, delivers multiple orders at once, and dodges Lagos traffic like a pro 😁.</p>
<p>So the next time someone asks you, “What is HTTP?”<br />Smile.<br />And tell them: “He’s my trusted delivery guy, carrying my web messages back and forth. Clean, clear, and fast.”</p>
<p>At least I can't be in the interview room with you...</p>
<p>But if this post helps you walk in more confidently, that’s good enough for me.</p>
<p>Check out the next part for <strong>HTTPS</strong><br />See you in the Next!</p>
]]></content:encoded></item><item><title><![CDATA[Understanding Database Types: When to Use What]]></title><description><![CDATA[Let me tell you a quick story. A team of developers launches a fitness app, and within the first month, it skyrockets to 10,000 users. Everything seems perfect right? …Great! until the app starts slowing down, users complaining, workouts fail to save...]]></description><link>https://code-along.hashnode.dev/understanding-database-types-when-to-use-what</link><guid isPermaLink="true">https://code-along.hashnode.dev/understanding-database-types-when-to-use-what</guid><category><![CDATA[DiceDB]]></category><category><![CDATA[Databases]]></category><category><![CDATA[DBMS]]></category><category><![CDATA[Relational Database]]></category><category><![CDATA[NoSQL]]></category><category><![CDATA[SQL]]></category><category><![CDATA[In-memory-database]]></category><category><![CDATA[Redis]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[Cassandra]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[database design]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Thu, 03 Apr 2025 11:12:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743677314591/2889714e-a6e9-4bd9-a19f-3f0132a3ca5c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let me tell you a quick story. A team of developers launches a fitness app, and within the first month, it skyrockets to 10,000 users. Everything seems perfect right? …Great! until the app starts slowing down, users complaining, workouts fail to save, and frustration builds.</p>
<p>The problem? The database wasn’t designed to handle real-time tracking from thousands of concurrent users. Instead of celebrating their success, the team scrambles for days, migrating to a better-suited system.</p>
<p>This scenario is more common than you might think. Choosing the right database isn’t just a technical decision—it directly impacts performance, scalability, and even your team's stress levels.</p>
<p>In this article, we'll explore the major database types in a way that's easy to understand. With real-world examples, you'll gain a clear framework for making database decisions that scale with your need</p>
<h2 id="heading-the-database-family-tree">The Database Family Tree</h2>
<p>Before diving into details, let's get a bird's-eye view of our options:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743676908600/3f2edfd3-f45c-4567-a727-a485f949478d.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Relational databases</strong>: The traditional organized filing cabinets (MySQL, PostgreSQL)</p>
</li>
<li><p><strong>Non-relational (NoSQL) databases</strong>: The flexible specialists, including:</p>
<ul>
<li><p>Document stores (MongoDB, Couchbase)</p>
</li>
<li><p>Key-value stores (DynamoDB, Riak)</p>
</li>
<li><p>Wide-column stores (Cassandra, HBase)</p>
</li>
<li><p>Graph databases (Neo4j, Amazon Neptune)</p>
</li>
</ul>
</li>
<li><p><strong>In-memory databases</strong>: The speed demons (Redis, Memcached)</p>
</li>
</ul>
<p>Each family exists because it solves specific problems better than the others. None is universally "best"—it's all about matching the right tool to your particular needs.</p>
<h2 id="heading-relational-databases-the-reliable-workhorses">Relational Databases: The Reliable Workhorses</h2>
<h3 id="heading-what-they-are">What They Are</h3>
<p>Imagine a collection of spreadsheets (tables) with strict relationships between them. Customer information in one table connects to orders in another via customer IDs. Every piece of data has its designated place, and there are rules to keep everything consistent. Think of it like a library where every book has exactly one correct shelf location, with a detailed catalog system ensuring you can find anything quickly</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743669182271/df14bde7-56da-4040-871f-4bc7e4b37ec5.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-when-they-shine">When They Shine</h3>
<p>Relational databases excel when your data is structured, relationships are clear, and data integrity is non-negotiable.</p>
<p><strong><em>Real-world example</em></strong>: Shopify powers millions of e-commerce stores using MySQL. When a customer places an order, multiple tables update simultaneously: inventory decreases, order history updates, payment processes, and customer information links to the new order. All of these operations must succeed or fail together—something relational databases handle beautifully through transactions.</p>
<p><strong><em>Another example</em></strong>: Bank of America uses Oracle to process financial transactions. When you transfer money, the bank must guarantee that the amount is deducted from one account and added to another—no exceptions. Relational databases provide <strong>ACID</strong> guarantees (Atomicity, Consistency, Isolation, Durability) that make this reliability possible.</p>
<h3 id="heading-when-they-struggle">When They Struggle</h3>
<p>Despite their strengths, relational databases face challenges when:</p>
<ul>
<li><p>Data doesn’t fit neatly into tables</p>
</li>
<li><p>Schema changes frequently</p>
</li>
<li><p>Horizontal scaling is needed across many servers</p>
</li>
<li><p>Extremely high write speeds are required</p>
</li>
</ul>
<p><strong><em>For example:</em></strong> Facebook originally used MySQL for everything but found it couldn't handle the complex, interconnected nature of a social graph at scale. The company eventually developed custom solutions, including graph databases for relationship data, because relational systems struggled with queries like, <em>"Show all posts from friends of friends who live in Lagos Nigeria and like biking."</em></p>
<h3 id="heading-popular-options">Popular Options</h3>
<ul>
<li><p><strong>PostgreSQL</strong>: Open-source, feature-rich, excellent for complex queries</p>
</li>
<li><p><strong>MySQL</strong>: Fast, reliable, and widely supported</p>
</li>
<li><p><strong>Oracle</strong>: Enterprise-grade with advanced features (and a price tag to match)</p>
</li>
<li><p><strong>Microsoft SQL Server</strong>: Deep Windows/Microsoft ecosystem integration</p>
</li>
</ul>
<h2 id="heading-non-relational-nosql-databases-the-flexible-specialists">Non-Relational (NoSQL) Databases: The Flexible Specialists</h2>
<p>NoSQL databases emerged to solve problems where relational databases struggled. Rather than offering one-size-fits-all solutions, they provide specialized tools for specific data patterns.</p>
<h3 id="heading-document-stores">Document Stores</h3>
<p><strong>What They Are:</strong> Collections of JSON-like documents where each document contains all relevant information about an entity.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743669265317/a96b0d28-7e0b-478a-b950-470400b849cd.png" alt class="image--center mx-auto" /></p>
<p><strong><em>Real-world example</em></strong>: The New York Times uses MongoDB to store articles and related content. Each article document contains the text, author info, related images, tags, and comments—everything needed to render that article. This approach makes it incredibly fast to retrieve and display complete articles without joining multiple tables.</p>
<p><strong>When They Shine:</strong> Content management, catalogs, user profiles, and any scenario where data items make natural "documents."</p>
<h3 id="heading-key-value-stores">Key-Value Stores</h3>
<p><strong>What They Are:</strong> Simple databases that store values associated with keys, similar to a giant dictionary.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743669522979/f3647859-f12e-4644-a849-a86890a3b945.png" alt class="image--center mx-auto" /></p>
<p><strong><em>Real-world example</em></strong>: Lyft uses Amazon DynamoDB to track driver locations in real-time. With millions of location updates per minute, they need blazing-fast writes and simple lookups by driver ID. The key-value model perfectly matches this pattern.</p>
<p><strong>When They Shine:</strong> Session storage, user preferences, shopping carts, and scenarios requiring simple, high-speed lookups.</p>
<h3 id="heading-wide-column-stores">Wide-Column Stores</h3>
<p><strong>What They Are:</strong> Databases that store data in columns rather than rows, allowing for massive scalability across distributed systems.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743670055539/54c780d6-af5e-4678-8e49-35f8cda1c680.png" alt class="image--center mx-auto" /></p>
<p><strong><em>Real-world example</em></strong>: Netflix uses Apache Cassandra to store and process viewer activity data. With 200+ million subscribers generating viewing events constantly, they need something that scales horizontally across hundreds of servers while handling enormous write loads. Cassandra’s column-oriented design makes it possible to add more servers as demand grows.</p>
<p><strong>When They Shine:</strong> Time-series data, event logging, and high-volume write scenarios requiring massive scale.</p>
<h3 id="heading-graph-databases">Graph Databases</h3>
<p><strong>What They Are:</strong> Specialized databases that excel at representing and querying complex relationships.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743670298182/08f0b27b-3971-4c4e-89d1-1c7450d4408d.png" alt class="image--center mx-auto" /></p>
<p><strong><em>Real-world example</em></strong>: LinkedIn's connection network runs on graph technology. When you see "3rd-degree connection" or receive suggestions for "people you may know," that’s a graph database efficiently traversing relationship paths that would require expensive joins in a relational database.</p>
<p><strong>When They Shine:</strong> Social networks, recommendation engines, fraud detection, and knowledge graphs.</p>
<h2 id="heading-in-memory-databases-the-speed-demons"><strong>In-Memory Databases: The Speed Demons</strong></h2>
<p><strong>What Makes Them Special:</strong><br />Most databases primarily store data on disk, reading it into memory as needed. In-memory databases flip this model by keeping everything in RAM, making them dramatically faster—often by 100x or more.</p>
<p><em>Real-World Examples:</em></p>
<ul>
<li><p>Twitter/X uses Redis to cache user timelines and handle millions of requests per second with sub-millisecond latency. When you refresh your timeline, you're likely seeing data served from Redis rather than hitting their main databases.</p>
</li>
<li><p>Airbnb uses Redis to power their search autocomplete. When you start typing "New Yo..." and instantly see "New York" suggestions, that's Redis responding in real-time.</p>
</li>
</ul>
<p><strong>When They Shine</strong><br />In-memory databases excel when:</p>
<ul>
<li><p>Speed is critical (e.g., trading platforms, gaming leaderboards)</p>
</li>
<li><p>Data access patterns are predictable</p>
</li>
<li><p>You need simple operations at massive scale</p>
</li>
<li><p>Caching frequently accessed data would improve performance</p>
</li>
</ul>
<p><strong>When They Struggle</strong><br />The tradeoffs come with:</p>
<ul>
<li><p>Limited capacity (RAM is expensive compared to disk storage)</p>
</li>
<li><p>Persistence challenges (though many now offer persistence options)</p>
</li>
<li><p>Data recovery after crashes (some in-memory data may be lost)</p>
</li>
</ul>
<h2 id="heading-making-the-decision-a-practical-framework">Making the Decision: A Practical Framework</h2>
<p>When choosing a database, ask yourself these questions:</p>
<ul>
<li><p>What's your data structure? Tabular with clear relationships → Relational; Nested or variable structure → Document; Simple values → Key-value; Complex relationships → Graph</p>
</li>
<li><p>What are your consistency needs? Strong consistency for financial or critical data → Relational; Eventual consistency acceptable → Many NoSQL options</p>
</li>
<li><p>What's your scale? Will you need to scale horizontally across many machines? Many NoSQL databases make this easier.</p>
</li>
<li><p>What are your query patterns? Complex joins and aggregations → Relational; Simple lookups by ID → Key-value; Relationship traversal → Graph</p>
</li>
</ul>
<h3 id="heading-hybrid-approaches"><strong>Hybrid Approaches</strong></h3>
<p>Most mature companies use multiple database types together:</p>
<p>Uber combines PostgreSQL for financial data and MySQL for transaction records, with Redis for real-time data like driver locations, and Cassandra for trip data requiring massive scale.<br />Airbnb uses MySQL for core booking data, Elasticsearch for searching listings, Redis for caching and real-time features, and Apache Druid for analytics.</p>
<h2 id="heading-case-study-evolution-of-database-architecture"><strong>Case Study: Evolution of Database Architecture</strong></h2>
<p>Let’s follow the journey of an imaginary fitness app as it scales:</p>
<p><strong>Stage 1: MVP (1,000 users)</strong><br />A single PostgreSQL database handles everything: user accounts, workout data, social features<br />Simple architecture gets the product launched quickly</p>
<p><strong>Stage 2: Growth (50,000 users)</strong><br />Added Redis for caching frequently accessed data<br />Still using PostgreSQL as the system of record<br />Response times improve, but database load during peak hours is concerning</p>
<p><strong>Stage 3: Scaling (500,000 users)</strong><br />Workout time-series data moved to MongoDB for better handling of variable workout structures<br />User graph (followers, friends) moved to Neo4j for better performance on social features<br />PostgreSQL still handles accounts, payments, and subscriptions<br />Redis expanded for real-time features like "users working out now"</p>
<p><strong>Stage 4: Enterprise (5+ million users)</strong><br />Time-series workout data migrated from MongoDB to Cassandra for better horizontal scaling<br />Redis Cluster implemented for distributed caching<br />PostgreSQL sharded by user region</p>
<h2 id="heading-conclusion-the-right-tool-for-the-right-job"><strong>Conclusion: The Right Tool for the Right Job</strong></h2>
<p>Choosing the right database isn’t about finding a perfect solution but understanding the strengths of each option. Relational databases are great for consistency, NoSQL excels with flexible, scalable data, and in-memory databases shine when speed is needed.</p>
<p>The fitness app team learned this the hard way. What began as a simple database soon became a bottleneck as the app grew. Reflecting on their experience, they realized they wouldn’t have faced that painful migration if they had understood their options from the start.</p>
<p>Six months later, their app supported 100,000 users with a hybrid system—PostgreSQL for user data, Redis for real-time features, and a time-series database for workout metrics. Their journey reinforced that there’s no one-size-fits-all database.</p>
<p>Now, with this knowledge, you can make informed choices based on your needs and growth. Start simple, but design with flexibility to scale. The right database lets you focus on building great features, not firefighting performance issues.</p>
]]></content:encoded></item><item><title><![CDATA[Why Job Queues In Your System Architecture?]]></title><description><![CDATA[As a backend engineer, you’ve likely faced tasks that take a long time to complete, causing delays in system response or even timeouts. These tasks don't always require immediate execution, but handling them within the same request can slow everythin...]]></description><link>https://code-along.hashnode.dev/why-job-queues-in-your-system-architecture</link><guid isPermaLink="true">https://code-along.hashnode.dev/why-job-queues-in-your-system-architecture</guid><category><![CDATA[ Bee-Queue]]></category><category><![CDATA[queue]]></category><category><![CDATA[System Architecture]]></category><category><![CDATA[Performance Optimization]]></category><category><![CDATA[Redis]]></category><category><![CDATA[bullmq]]></category><category><![CDATA[Backend Engineering]]></category><category><![CDATA[scalability]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Sun, 13 Oct 2024 21:17:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728853797369/e426b7f5-54f6-47cf-af29-2e15f2981209.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a backend engineer, you’ve likely faced tasks that take a long time to complete, causing delays in system response or even timeouts. These tasks don't always require immediate execution, but handling them within the same request can slow everything down.</p>
<p>While growing as a backend engineer, I faced similar challenges. For example, generating large reports or pulling detailed account statements often led to timeouts. Why? Because I was processing them in the request instead of taking them off to be handled in the background. This is where job/worker queues come in to save the day.</p>
<p>By moving these long-running tasks to the background using a queue, you can free up your system to handle other requests quickly and efficiently. This not only improves performance but also enhances user experience by ensuring the system responds promptly without making them wait.</p>
<p>In this article, I’ll walk you through the concept of job/worker queues, how Redis can be used as the backbone for this system, and how introducing queues into your system design can significantly improve backend performance.</p>
<h3 id="heading-understanding-queuing-in-system-design">Understanding Queuing in System Design</h3>
<p>Before diving into job queues specifically, it’s important to understand the broader concept of queues and their role in system design.</p>
<p>In computer science, a <strong>queue</strong> is a data structure that works on a first-in, first-out (FIFO) basis. Think of it as a line of people waiting for a service—the first person to arrive is the first to be served. This simple mechanism allows systems to manage and prioritize tasks in an orderly fashion.</p>
<p>There are several types of queues, each suited to different use cases:</p>
<ul>
<li><p><strong>Message Queues</strong>: Facilitate communication between distributed systems by allowing asynchronous message exchange without direct dependency.<br />  <strong><em>Example</em></strong>: Microservices using RabbitMQ or Apache Kafka.</p>
</li>
<li><p><strong>Job Queues</strong>: Offload time-consuming tasks to background workers, preventing them from blocking the main application.<br />  <strong><em>Example</em></strong>: Sending emails, processing payments or generating reports in the background.</p>
</li>
<li><p><strong>Task Queues</strong>: Manage smaller, immediate tasks that can be processed in parallel.<br />  <strong><em>Example</em></strong>: Resizing images or updating profiles in bulk.</p>
</li>
<li><p><strong>Priority Queues</strong>: Handle tasks based on priority, ensuring critical tasks are processed first.<br />  <strong><em>Example</em></strong>: Triggering alerts for system failures before handling routine events.</p>
</li>
</ul>
<p>Each type enhances system performance by decoupling tasks and optimizing processing time.</p>
<h3 id="heading-what-are-job-queues-and-why-do-you-need-them">What Are Job Queues and Why Do You Need Them?</h3>
<p>Job queues allow you to offload time-consuming tasks to be processed asynchronously, meaning they run in the background, freeing up your system to handle other requests. This is crucial for tasks like:</p>
<ul>
<li><p>Sending emails</p>
</li>
<li><p>Generating reports</p>
</li>
<li><p>Processing payments</p>
</li>
<li><p>Importing/exporting data</p>
</li>
</ul>
<p>Think of it this way: you don’t want a user waiting on a page while an email is sent or a large report is generated. Instead, you can queue the task and let your system respond immediately, then complete the task later in the background.</p>
<h3 id="heading-more-real-world-scenarios">More Real-world Scenarios</h3>
<p>Imagine the following scenarios where job queues can make a significant difference:</p>
<ul>
<li><p><strong>Large E-commerce Platform During Peak Sales:</strong> Imagine a large e-commerce platform where users place orders during peak sales, such as Black Friday. Instead of processing each order and sending a confirmation email immediately, the system can offload tasks like sending confirmation emails, updating inventory, and notifying shipping services to the background. This ensures that the platform remains responsive for users continuing to browse or place orders.</p>
</li>
<li><p><strong>User Registration with Email Verification:</strong> When users register for a new account, a verification email needs to be sent. Without a queue, the system might make users wait while the email is sent. By queuing the email task, the system can respond instantly with a welcome message, while the email is sent in the background, improving the user experience.</p>
</li>
<li><p><strong>Financial Services App for Generating Account Statements:</strong> In a banking or fintech application, users may request detailed account statements that span several months or years. Generating these reports can be resource-heavy, but by queuing the task, users can continue using other features of the app while the report is prepared in the background, reducing system load and ensuring faster response times for other users.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728852412157/6f99820c-b841-425a-921a-00868fbcc710.png" alt="Visualization of a request-queue workflow where multiple client requests are added to a single job queue. Concurrent workers process tasks from the queue, interact with the customer's database, and return results through various channels such as reports or email notifications." class="image--center mx-auto" /></p>
</li>
</ul>
<h3 id="heading-redis-as-the-backbone-for-job-queues">Redis as the Backbone for Job Queues</h3>
<p>Redis, an in-memory data structure store, is perfect for building job/worker queues. Why? Because Redis is:</p>
<ul>
<li><p><strong>Fast</strong>: Redis stores data in memory, making it quick to enqueue and dequeue jobs.</p>
</li>
<li><p><strong>Reliable</strong>: It supports durability, so queued jobs are not lost even if your system crashes.</p>
</li>
<li><p><strong>Scalable</strong>: Redis can handle a large number of jobs and workers distributed across multiple servers.</p>
</li>
</ul>
<p>Redis supports list operations like <code>LPUSH</code>, <code>LPOP</code>, and <code>BRPOP</code>, making it easy to implement a basic queue. However, using Redis with some purpose-built tools will make your life as a backend engineer even easier.</p>
<p>Now that we understand Redis as the backbone for job queues, let’s explore how tools like BullMQ simplify job management in Node.js</p>
<h3 id="heading-tools-built-on-redis-for-job-queues">Tools Built on Redis for Job Queues</h3>
<p>Let’s look at two popular tools built on Redis that will simplify working with job queues in your backend system:</p>
<h5 id="heading-1-bullmq-for-nodejs">1. <strong>BullMQ</strong> (for Node.js)</h5>
<p>One of the most powerful and widely used job queue libraries is <strong>BullMQ</strong>, built on Redis. It’s great for handling tasks asynchronously in Node.js applications. Whether you need to send notifications, resize images, or schedule tasks, BullMQ provides the tools to do it efficiently.</p>
<p>Here’s how BullMQ can improve your system:</p>
<ul>
<li><p><strong>Job retries</strong>: Automatically retry failed tasks.</p>
</li>
<li><p><strong>Delayed jobs</strong>: Schedule tasks to be executed later, not immediately.</p>
</li>
<li><p><strong>Concurrency</strong>: Process multiple jobs in parallel, reducing bottlenecks.</p>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> { Queue } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'bullmq'</span>);

<span class="hljs-comment">// Create a new queue</span>
<span class="hljs-keyword">const</span> reportQueue = <span class="hljs-keyword">new</span> Queue(<span class="hljs-string">'reportQueue'</span>);

<span class="hljs-comment">// Add a job to the queue</span>
reportQueue.add(<span class="hljs-string">'generate-report'</span>, { userId: <span class="hljs-number">12345</span> });
</code></pre>
<p>With BullMQ, you can create <strong>multiple workers</strong> to consume tasks from the same queue. This allows you to scale up your system, as workers can run in parallel, processing tasks independently. This setup ensures tasks are completed faster, without overloading your main application.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Worker } <span class="hljs-keyword">from</span> <span class="hljs-string">'bullmq'</span>;

<span class="hljs-comment">// Create a worker to process jobs from the queue</span>
<span class="hljs-keyword">const</span> worker1 = <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">'reportQueue'</span>, <span class="hljs-keyword">async</span> job =&gt; {
  <span class="hljs-comment">// process the job here</span>
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Worker 1 is processing job <span class="hljs-subst">${job.id}</span>`</span>);
});

<span class="hljs-comment">// Create a second worker to consume from the same queue</span>
<span class="hljs-keyword">const</span> worker2 = <span class="hljs-keyword">new</span> Worker(<span class="hljs-string">'reportQueue'</span>, <span class="hljs-keyword">async</span> job =&gt; {
  <span class="hljs-comment">// process the job here</span>
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Worker 2 is processing job <span class="hljs-subst">${job.id}</span>`</span>);
});
</code></pre>
<p>In this example, <strong>worker1</strong> and <strong>worker2</strong> both consume tasks from the report queue. If the queue has many jobs, the workers will split the load, improving processing time and efficiency.</p>
<h5 id="heading-2-bee-queue-nodejs">2. <strong>Bee-Queue</strong> (Node.js)</h5>
<p><strong>Bee-Queue</strong> is another lightweight, Redis-backed job queue for Node.js. It’s fast, with low overhead, making it ideal for simple use cases where you don’t need the extra complexity of tools like Bull.</p>
<p>While it’s simpler, Bee-Queue still gives you the essentials: delayed jobs, job retries, and concurrency. It works well for small-scale applications or services where simplicity is key.</p>
<h3 id="heading-why-job-queues-improve-backend-performance">Why Job Queues Improve Backend Performance</h3>
<p>Integrating job queues into your system architecture has several advantages:</p>
<ol>
<li><p><strong>Improved Response Times</strong>: Offloading tasks to a queue ensures that your API responds quickly to user requests, while background workers handle the heavy lifting behind the scenes.</p>
</li>
<li><p><strong>Scalability</strong>: Redis-based job queues allow you to scale horizontally. You can add more workers to handle more jobs as your system grows, without adding pressure on your core application.</p>
</li>
<li><p><strong>Failure Handling</strong>: Tools like BullMQ allow for job retries and dead-letter queues (DLQ). If a task fails, it can be retried a certain number of times, and if it still fails, it’s moved to a DLQ where it can be reviewed manually.</p>
</li>
</ol>
<h3 id="heading-designing-your-backend-with-job-queues">Designing Your Backend with Job Queues</h3>
<p>When building or scaling your backend system, consider when and where you can introduce job queues. Typical scenarios where queues can dramatically improve system performance include:</p>
<ul>
<li><p><strong>Heavy computational tasks</strong>: Media processing is CPU-intensive. Queue these tasks so they don’t block your server’s performance. Tasks like report generation or video processing can be offloaded to a queue, ensuring users don’t experience long waits or timeouts.</p>
</li>
<li><p><strong>Third-party API interactions</strong>: API calls to third-party services (e.g., payment gateways) can be unreliable or slow. Queue them to ensure the request goes through without impacting your system’s response time.</p>
</li>
<li><p><strong>Scheduled tasks</strong>: Need to run scheduled tasks like sending daily summaries or alerts? Queues can handle this efficiently, without adding extra complexity to your application logic. Emails, SMS, or push notifications don’t need to be sent instantly. Queue these tasks to avoid slowing down your app.</p>
</li>
<li><p><strong>Data Import/Export</strong>: Handling large files or data exports can take time, and it’s better to let a worker process it in the background.</p>
</li>
</ul>
<h3 id="heading-conclusion">Conclusion</h3>
<p>To summarise, Redis-based job queues, such as BullMQ and Bee-Queue, are vital tools in modern backend system design. They improve performance by handling background tasks asynchronously, reduce system load during peak times, and provide a reliable mechanism for scaling workers.</p>
<p>Incorporating job queues into your system design improves response times, ensures reliability, and allows you to scale your application effortlessly as it grows. Next time you’re faced with a task that could slow down your app, remember—queue it!</p>
<p>Have you encountered long-running tasks that slow down your system? How would implementing job queues benefit your current projects? Let me know in the comments.</p>
<h3 id="heading-further-reading">Further Reading</h3>
<p>If you're interested in diving deeper into implementing BullMQ in an Express.js application, be sure to check out the <strong>next post</strong> for a detailed guide.</p>
]]></content:encoded></item><item><title><![CDATA[Idempotency in API Design: Ensuring Reliable and Predictable Systems]]></title><description><![CDATA[As a software engineer, ensuring that your systems behave predictably and reliably is crucial, especially when designing APIs. One key concept that helps you achieve this is idempotency. But what exactly is idempotency, and why is it important for AP...]]></description><link>https://code-along.hashnode.dev/idempotency-in-api-design-ensuring-reliable-and-predictable-systems</link><guid isPermaLink="true">https://code-along.hashnode.dev/idempotency-in-api-design-ensuring-reliable-and-predictable-systems</guid><category><![CDATA[API Design]]></category><category><![CDATA[idempotency]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Backend Development]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Fri, 13 Sep 2024 14:58:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726235651815/8246448a-b16c-4469-8d3f-6973cb04c336.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a software engineer, ensuring that your systems behave predictably and reliably is crucial, especially when designing APIs. One key concept that helps you achieve this is <strong>idempotency</strong>. But what exactly is idempotency, and why is it important for API design? Let’s break it down.</p>
<h2 id="heading-what-is-idempotency">What is Idempotency?</h2>
<p>Idempotency is a property of an operation that guarantees the same result, no matter how many times you perform the same operation. In other words, repeating an operation won’t have any additional side effects beyond the first execution.</p>
<p>Still sound like jargon? Let’s make it simpler.</p>
<p>Now, imagine you’re pouring a cup of coffee. Once the cup is full, pouring more coffee doesn’t change the fact that the cup is already full. Whether you try to fill the cup once or multiple times, the result remains the same—the cup is already at capacity.</p>
<p>This is the essence of idempotency: repeating the same action doesn’t alter the outcome after the first successful attempt.</p>
<p>Got it now? Yea… awesome!</p>
<h2 id="heading-idempotency-in-api-design"><strong>Idempotency in API Design</strong></h2>
<p>When designing your APIs, especially REST APIs, idempotency is critical for making sure operations are safe and predictable. The HTTP protocol specifies which methods should be idempotent by nature and which ones are not. Here’s what you need to know:</p>
<ul>
<li><p><strong>GET</strong>: Retrieving data should always be idempotent. Every time you request the same resource, you should get the same result without changing the state of the server.</p>
</li>
<li><p><strong>DELETE</strong>: Deleting a resource is idempotent because once the resource is deleted, any further <code>DELETE</code> requests should confirm that the resource no longer exists any errors or additional changes.</p>
</li>
<li><p><strong>PUT</strong>: Updating or replacing a resource with <code>PUT</code> is idempotent. If you send the same update multiple times, the resource should remain in the same state after the first successful update.</p>
</li>
<li><p><strong>POST</strong>: <code>POST</code> is typically <strong>not idempotent</strong>. This method often creates new resources, so if the request is repeated, you might end up with multiple identical resources. However, with proper handling, you can design certain <code>POST</code> operations to behave idempotently.</p>
</li>
</ul>
<h2 id="heading-why-idempotency-matters"><strong>Why Idempotency Matters</strong></h2>
<p>Now that you understand what idempotency is, let’s explore why it matters. Imagine you’re ordering food online. Once you confirm your order, you expect it to be processed only once, no matter how many times you click the “Confirm Order” button. However, In real-world systems, things don’t always go smoothly. Network interruptions or timeouts might cause a client to resend a request, and if your APIs aren’t designed to handle these retries properly, you could run into trouble—duplicate orders, payments processed multiple times, or a messed-up system state.</p>
<ul>
<li><p><strong>Handling Retries Safely</strong>: It helps handle network issues where clients might retry requests due to timeouts or connectivity problems. Idempotent APIs ensure no unintended side effects occur due to retries.</p>
</li>
<li><p><strong>Maintaining Consistency</strong>: In modern cloud-based architectures, multiple instances of services may be running simultaneously, and the same request might be processed by different servers. Idempotency ensures that no matter how many instances process the same request, the outcome remains consistent.</p>
</li>
<li><p><strong>Preventing Data Corruption</strong>: By designing idempotent APIs, you reduce the risk of issues like data duplication, corruption, or errors when clients retry requests. This increases the overall reliability of the system, making it more robust and predictable.</p>
</li>
</ul>
<h2 id="heading-real-world-example-of-idempotency"><strong>Real-World Example of Idempotency</strong></h2>
<p>Let’s take a practical example. Consider an API for updating a user’s email address:</p>
<pre><code class="lang-typescript">PUT /users/<span class="hljs-number">123</span>/email
{
  <span class="hljs-string">"email"</span>: <span class="hljs-string">"email23@example.com"</span>
}
</code></pre>
<p>In this case, <code>PUT</code> is idempotent. Whether you send this request once or 100 times with the same payload, the user’s email will remain <code>"</code><a target="_blank" href="mailto:newemail@example.com"><code>email23@example.com</code></a><code>"</code>.</p>
<p>Now, compare this to creating a new user:</p>
<pre><code class="lang-typescript">POST /users
{
  <span class="hljs-string">"name"</span>: <span class="hljs-string">"John Doe"</span>,
  <span class="hljs-string">"email"</span>: <span class="hljs-string">"johndoe@example.com"</span>
}
</code></pre>
<p>Each time this <code>POST</code> request is sent, a new user might be created, leading to multiple identical user records. This non-idempotent behaviour can cause problems in systems that don't properly manage retries.</p>
<h2 id="heading-designing-idempotent-apis"><strong>Designing Idempotent APIs</strong></h2>
<p>To ensure your API is idempotent, you can use techniques like:</p>
<ul>
<li><p><strong>Idempotency keys</strong>: For example, in payment systems, you can generate a unique idempotency key for each transaction. If the client retries the same transaction with the same key, the server can recognize it and return the original response rather than processing it again.</p>
</li>
<li><p><strong>Proper use of HTTP methods</strong>: Always use “<code>PUT</code>" for updating resources and <code>POST</code> for creating resources. This naturally enforces idempotent behaviour for updates.</p>
</li>
<li><p><strong>Handling edge cases</strong>: Even if a method like <code>POST</code> is not idempotent, you can design it to behave safely in retries by checking for duplicate requests or adding constraints that prevent multiple creations of the same resource.</p>
</li>
</ul>
<h2 id="heading-a-real-life-example-fixing-duplicate-payment-notifications"><strong>A Real-Life Example: Fixing Duplicate Payment Notifications</strong></h2>
<p>To illustrate, let’s revisit a scenario from a product I have worked on. The application had a <code>POST</code> endpoint to handle payment webhook notifications. Due to network issues, the payment provider sometimes resents the same notification, leading to duplicate values given to the user.</p>
<p>Here’s how I tackled the problem:</p>
<ul>
<li><p>Each payment transaction was assigned a unique <strong>idempotency key</strong>, similar to a unique booking reference.</p>
</li>
<li><p>The key is included in the provider’s webhook notification</p>
</li>
<li><p>Before processing the webhook, the application checked whether the idempotency key had already been processed.</p>
</li>
</ul>
<p>If the key was already processed, the system skipped further actions, preventing duplicate values for the user. This ensured that users only received value once, even if the webhook was resent.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Idempotency is a cornerstone of reliable API design. By ensuring that repeated operations don’t produce unexpected side effects, you can make your systems more robust, handle retries safely, and maintain consistency across distributed environments. Whether you’re working with payments, user data, or any critical operation, designing for idempotency is key to preventing common issues like data duplication and corruption.</p>
<p>As you design your APIs, keep idempotency in mind to ensure your system behaves predictably no matter what challenges come its way.</p>
]]></content:encoded></item><item><title><![CDATA[Solutions To Lost Update Concurrency Problem]]></title><description><![CDATA[What is Concurrency?
Concurrency involves multiple processes or transactions executing at the same time, potentially interacting with shared data. Imagine a social media platform where several users react to a post simultaneously. Each reaction is an...]]></description><link>https://code-along.hashnode.dev/solutions-to-lost-update-concurrency-problem</link><guid isPermaLink="true">https://code-along.hashnode.dev/solutions-to-lost-update-concurrency-problem</guid><category><![CDATA[Databases]]></category><category><![CDATA[concurrency]]></category><category><![CDATA[concurrency-control]]></category><category><![CDATA[backend]]></category><category><![CDATA[MySQL]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[MongoDB]]></category><category><![CDATA[Sequelize]]></category><category><![CDATA[mongoose]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Fri, 16 Aug 2024 23:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724511902750/bb29a4f5-9485-4498-815b-dcbf5c1711bb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-what-is-concurrency">What is Concurrency?</h3>
<p>Concurrency involves multiple processes or transactions executing at the same time, potentially interacting with shared data. Imagine a social media platform where several users react to a post simultaneously. Each reaction is an operation that affects the post’s reaction count.</p>
<h3 id="heading-the-lost-update-problem">The Lost Update Problem</h3>
<p>In simple terms, the update of a client's request is overwritten by the update of another. <mark>This is also referred to as </mark> <strong><mark>RACE CONDITIONS</mark></strong></p>
<p>Let’s consider a simple scenario to illustrate the Lost Update Problem. Suppose you have a post with a reaction count of 100. Two users simultaneously decide to like and dislike the post respectively. Ideally, after these reactions are processed, the count should return to 100. However, without proper concurrency control, one of these reactions might be lost, and the final count might not reflect all reactions accurately. This issue occurs because multiple transactions (or operations) are trying to update the same data at the same time without proper coordination.</p>
<h3 id="heading-the-problem-in-action">The Problem in Action</h3>
<p>To better understand the Lost Update Problem, let’s walk through a simple scenario using the example of reacting to a social media post.</p>
<p>Imagine a post that currently has 100 likes. Two clients, A and B, both decide to interact with this post at almost the same time, but with different actions—Client A wants to like the post, while Client B decides to un-like it. Below is a table that shows the steps each client takes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724508558387/715d2bdb-b48d-4116-9f44-7fe72ccf908b.png" alt="This table illustrates the Lost Update Problem, where multiple clients (Client A and Client B) are concurrently reacting to a post by liking it. The table shows the initial reaction count before each client's action and the final reaction count after their action. The issue arises when concurrent updates lead to an incorrect final count, as shown in the scenario." /></p>
<ul>
<li><p><strong>Step 1</strong>: Client A reads the current like count (x = 100) and increments it by 1 in memory (x = 101). At the same time, Client B reads the same current like count (x = 100) and decrements it by 1 in memory (x = 99).</p>
</li>
<li><p><strong>Step 2</strong>: Client B writes the updated count (x = 99) to the database.</p>
</li>
<li><p><strong>Step 3</strong>: Client A then writes its updated count (x = 101) to the database.</p>
</li>
</ul>
<p><strong>The Result</strong>: Instead of the expected behaviour, where the final like count should return to 100, it ends up being 101—Client A’s write operation overwrote Client B’s, effectively losing Client B’s update.</p>
<p>This is a classic example of the Lost Update Problem, where concurrent transactions interfere with each other, leading to incorrect data being written to the database.</p>
<h3 id="heading-solutions-to-the-problem">Solutions to the Problem</h3>
<p>To address the Lost Update Problem, various strategies can be employed, each with its trade-offs. Let’s explore four key approaches:</p>
<h3 id="heading-using-atomic-operations">Using Atomic Operations</h3>
<p>Atomic operations ensure that an update is performed as a single, indivisible unit. This means that each reaction is handled independently, and the increment operation is performed without interference from other operations.</p>
<p><strong>Example:</strong></p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Increment the reaction count atomically</span>
<span class="hljs-keyword">UPDATE</span> posts <span class="hljs-keyword">SET</span> like_count = like_count + <span class="hljs-number">1</span> <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span>;
</code></pre>
<p><strong>Sequelize (MySQL/PostgreSQL) Example</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> Post.update(
  { like_count: Sequelize.literal(<span class="hljs-string">'like_count + 1'</span>) },
  { where: { id: <span class="hljs-number">1</span> } }
);
</code></pre>
<p><strong>Mongoose (MongoDB) Example</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> Post.findByIdAndUpdate(
  <span class="hljs-number">1</span>,
  { $inc: { like_count: <span class="hljs-number">1</span> } },
  { <span class="hljs-keyword">new</span>: <span class="hljs-literal">true</span> }
);
</code></pre>
<p><strong><mark>Pros</mark></strong>: Simple and efficient, especially for straightforward updates.</p>
<p><strong><mark>Cons</mark></strong>: Limited to basic operations and may not handle complex business logic.</p>
<h3 id="heading-using-pessimistic-locking">Using Pessimistic Locking</h3>
<p>Pessimistic locking involves putting a lock on a record or document that is selected by a query until the transaction is committed or rolled back. It prevents other transactions from updating or deleting the record.</p>
<p>This type of approach uses transactions. In SQL-based databases, this is often done using the <code>FOR UPDATE</code> clause.</p>
<p><strong>Raw SQL Query Example with</strong> <code>FOR UPDATE</code>:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">BEGIN</span>;

<span class="hljs-keyword">SELECT</span> like_count 
<span class="hljs-keyword">FROM</span> posts 
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span> 
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">UPDATE</span>;

<span class="hljs-keyword">UPDATE</span> posts 
<span class="hljs-keyword">SET</span> like_count = like_count + <span class="hljs-number">1</span> 
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span>;

<span class="hljs-keyword">COMMIT</span>;
</code></pre>
<p><strong>Sequelize (MySQL/PostgreSQL) Example</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> transaction = <span class="hljs-keyword">await</span> sequelize.transaction();

<span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">const</span> post = <span class="hljs-keyword">await</span> Post.findOne({
    where: { id: <span class="hljs-number">1</span> },
    lock: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Locking the row for update</span>
    transaction
  });

  post.like_count += <span class="hljs-number">1</span>;
  <span class="hljs-keyword">await</span> post.save({ transaction });

  <span class="hljs-keyword">await</span> transaction.commit();
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-keyword">await</span> transaction.rollback();
  <span class="hljs-keyword">throw</span> error;
}
</code></pre>
<p><strong><mark>Mongoose (MongoDB) Example</mark></strong><mark>: Pessimistic locking is not natively supported in MongoDB as it is in SQL databases, but you can simulate it with a custom locking mechanism.</mark></p>
<h3 id="heading-types-of-pessimistic-locks"><strong>Types of Pessimistic Locks</strong>:</h3>
<ul>
<li><p><strong>Exclusive Lock (Write Lock)</strong>: This type of lock prevents other transactions from both reading and writing to the locked resource. It is typically used when the transaction intends to modify the data.</p>
</li>
<li><p><strong>Shared Lock (Read Lock)</strong>: A shared lock allows other transactions to read the data but not to modify it. Multiple transactions can acquire a shared lock on the same resource, but no transaction can change it until all shared locks are released.</p>
</li>
</ul>
<p>In Sequelize ORM, the <code>lock</code> option can be used to specify the type of lock when querying the database. The value passed to this option depends on the database being used:</p>
<ul>
<li><p>For <strong>MySQL</strong>: <code>LOCK.UPDATE</code> for an exclusive lock and <code>LOCK.SHARE</code> for a shared lock.</p>
</li>
<li><p>For <strong>PostgreSQL</strong>: <code>LOCK.UPDATE</code> for an exclusive lock and <code>LOCK.KEY_SHARE</code> for a shared lock.</p>
</li>
</ul>
<p><strong><mark>Pros</mark></strong>: Guarantees that no other transactions can interfere.<br /><strong><mark>Cons</mark></strong>: Can reduce concurrency and lead to potential deadlocks—a situation where two or more transactions are waiting for each other to release lock</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> Post.findOne({
  where: { id: <span class="hljs-number">1</span> },
  lock: sequelize.Transaction.LOCK.UPDATE, <span class="hljs-comment">// Exclusive Lock</span>
  transaction
});
</code></pre>
<h3 id="heading-using-serializable-isolation-level">Using Serializable Isolation Level</h3>
<p>The isolation level in a DBMS determines how transactions interact with each other and the level of visibility they have over one another. Higher isolation levels reduce the chances of concurrency issues but can impact performance.</p>
<ul>
<li><p><strong>Serializable</strong> is the highest isolation level, which handles transactions as if they are executed one after another. It effectively prevents concurrency problems like the Lost Update Problem <mark>but can reduce throughput due to the strict control.</mark></p>
</li>
<li><p>Other levels include <strong>Repeatable Read</strong>, <strong>Read Committed</strong>, and <strong>Read Uncommitted</strong>.</p>
</li>
<li><p><strong>Read Uncommitted</strong> is the lowest level, offering maximum performance but introducing more concurrency problems like dirty reads and lost updates.</p>
</li>
</ul>
<p><strong>Raw SQL Example</strong>:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SET</span> <span class="hljs-keyword">TRANSACTION</span> <span class="hljs-keyword">ISOLATION</span> <span class="hljs-keyword">LEVEL</span> <span class="hljs-keyword">SERIALIZABLE</span>;

<span class="hljs-keyword">BEGIN</span>;

<span class="hljs-keyword">SELECT</span> like_count 
<span class="hljs-keyword">FROM</span> posts 
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span>;

<span class="hljs-keyword">UPDATE</span> posts 
<span class="hljs-keyword">SET</span> like_count = like_count + <span class="hljs-number">1</span> 
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span>;

<span class="hljs-keyword">COMMIT</span>;
</code></pre>
<p>In this example, the <code>SERIALIZABLE</code> isolation level ensures that no other transaction can read or write to the <code>posts</code> table until the transaction is completed.</p>
<p><strong>Sequelize Example</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> transaction = <span class="hljs-keyword">await</span> sequelize.transaction({
  isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE
});

<span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">const</span> post = <span class="hljs-keyword">await</span> Post.findOne({
    where: { id: <span class="hljs-number">1</span> },
    transaction
  });

  post.like_count += <span class="hljs-number">1</span>;
  <span class="hljs-keyword">await</span> post.save({ transaction });

  <span class="hljs-keyword">await</span> transaction.commit();
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-keyword">await</span> transaction.rollback();
  <span class="hljs-comment">// Handle concurrency conflict (e.g., retry or notify user)</span>
}
</code></pre>
<p>In Sequelize, you can set the isolation level by passing the <code>isolationLevel</code> option to the <code>transaction</code> method.</p>
<p><strong>Mongoose (MongoDB) Example</strong>:<br />Mongoose, being built on MongoDB, doesn't directly support transaction isolation levels like traditional relational databases. However, MongoDB offers some degree of isolation by default when using transactions.</p>
<p><strong><mark>Pros</mark></strong>: Ensures complete isolation and prevents all other concurrency problems.<br /><strong><mark>Cons</mark></strong>: Can significantly impact performance and reduce concurrency.</p>
<h3 id="heading-using-optimistic-locking">Using Optimistic Locking</h3>
<p>In this approach, multiple transactions proceed without locking resources. It assumes that conflicts are rare and checks for them only before committing changes. If a conflict is detected (e.g., another transaction has modified the data), the transaction is rolled back, and the operation can be retried.</p>
<p>It is typically implemented using a versioning mechanism where each record or document has a version or last update timestamp field. This field is checked during the update, and if it doesn't match the expected value, the transaction is aborted.</p>
<p><strong>Raw SQL Example</strong>:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">BEGIN</span>;

<span class="hljs-keyword">SELECT</span> like_count, <span class="hljs-keyword">version</span> 
<span class="hljs-keyword">FROM</span> posts 
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span>;

<span class="hljs-comment">-- Assume version = 5</span>
<span class="hljs-keyword">UPDATE</span> posts 
<span class="hljs-keyword">SET</span> like_count = like_count + <span class="hljs-number">1</span>, 
    <span class="hljs-keyword">version</span> = <span class="hljs-keyword">version</span> + <span class="hljs-number">1</span> 
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = <span class="hljs-number">1</span> <span class="hljs-keyword">AND</span> <span class="hljs-keyword">version</span> = <span class="hljs-number">5</span>;

<span class="hljs-keyword">COMMIT</span>;
</code></pre>
<p>In this example, the <code>UPDATE</code> statement ensures that the <code>version</code> hasn't changed since it was last read. If the <code>version</code> has changed, the update will fail, signalling a conflict.</p>
<p><strong>Sequelize Example</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> post = <span class="hljs-keyword">await</span> Post.findOne({
  where: { id: <span class="hljs-number">1</span> }
});

<span class="hljs-keyword">const</span> originalVersion = post.version;

post.like_count += <span class="hljs-number">1</span>;

<span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">await</span> post.save({
    where: {
      id: post.id,
      version: originalVersion
    }
  });
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-comment">// Handle concurrency conflict (e.g., retry or notify user)</span>
}
</code></pre>
<p>In Sequelize, optimistic locking can be implemented by manually checking the <code>version</code> field before updating the record. The update will proceed only if the version in the database matches the original version.</p>
<p><strong>Mongoose Example</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> post = <span class="hljs-keyword">await</span> Post.findById(id);

<span class="hljs-keyword">const</span> originalVersion = post.__v; <span class="hljs-comment">// Mongoose uses __v for versioning by default</span>

post.like_count += <span class="hljs-number">1</span>;

<span class="hljs-keyword">try</span> {
  <span class="hljs-keyword">await</span> post.save(); <span class="hljs-comment">// Mongoose automatically checks and updates __v</span>
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-comment">// Handle concurrency conflict (e.g., retry or notify user)</span>
}
</code></pre>
<p>Mongoose has built-in support for optimistic locking through versioning (<code>__v</code> field). When you save a document, Mongoose automatically checks the version and throws an error if there's a version mismatch.</p>
<p><strong><mark>Pros</mark></strong>:</p>
<ul>
<li><p>One of the key advantages of optimistic locking is that it allows you to gracefully handle concurrency issues. For example, if a conflict is detected, you can notify the client that the data has been updated by someone else. This allows the client to review the changes and decide how to proceed, which enhances the user experience.</p>
</li>
<li><p>Allows greater concurrency and is suitable for scenarios where conflicts are rare.</p>
</li>
</ul>
<p><strong><mark>Cons</mark></strong>: Requires additional logic to handle conflicts and retries.</p>
<h3 id="heading-more-reads">More Reads</h3>
<p>If you’re interested in diving deeper into the topics covered in this article, here are some additional resources:</p>
<ul>
<li><p><a target="_blank" href="https://on-systems.tech/blog/128-preventing-read-committed-sql-concurrency-errors/">https://on-systems.tech/blog/128-preventing-read-committed-sql-concurrency-errors/</a></p>
</li>
<li><p><a target="_blank" href="https://www.geeksforgeeks.org/concurrency-problems-in-dbms-transactions/">https://www.geeksforgeeks.org/concurrency-problems-in-dbms-transactions/</a></p>
</li>
<li><p><a target="_blank" href="https://www.javatpoint.com/dbms-concurrency-control">https://www.javatpoint.com/dbms-concurrency-control</a></p>
</li>
</ul>
<h3 id="heading-conclusion">Conclusion</h3>
<p>Concurrency is an important concept that drives the performance of modern applications but also brings challenges such as the Lost Update Problem. By employing strategies like atomic operations, pessimistic locking, isolation levels, and optimistic locking, you can effectively manage these challenges and ensure data accuracy in concurrent environments.</p>
<p>Ultimately, the best solution depends on your application's specific needs and limitations. Knowing these techniques and how they work together is crucial for building strong, high-performance systems.</p>
<p>If there's anything you believe needs a more detailed explanation, or if you spot any errors, feel free to leave a comment. Your feedback is invaluable!</p>
<p>if you found this article helpful, a little love ❤️ would be greatly appreciated.</p>
]]></content:encoded></item><item><title><![CDATA[How to Upload Files to Any Cloud Storage Platform Using express-file-wizardry in Express.js]]></title><description><![CDATA[File upload is a crucial aspect of web development enabling users to move files from their device to a server, necessary for various uses like profile picture uploads or submitting online forms.
To ensure secure and dependable storage of uploaded fil...]]></description><link>https://code-along.hashnode.dev/how-to-upload-files-to-any-cloud-storage-platform-using-express-file-wizardry-in-expressjs</link><guid isPermaLink="true">https://code-along.hashnode.dev/how-to-upload-files-to-any-cloud-storage-platform-using-express-file-wizardry-in-expressjs</guid><category><![CDATA[express-file-wizardry]]></category><category><![CDATA[Express.js]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[File Upload]]></category><category><![CDATA[cloudinary]]></category><category><![CDATA[Amazon S3]]></category><category><![CDATA[google cloud]]></category><dc:creator><![CDATA[Usman Soliu]]></dc:creator><pubDate>Sat, 02 Mar 2024 16:02:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709394684595/b4da5d32-e80d-4e69-8640-bb3cb98bef55.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>File upload is a crucial aspect of web development enabling users to move files from their device to a server, necessary for various uses like profile picture uploads or submitting online forms.</p>
<p>To ensure secure and dependable storage of uploaded files, integration with cloud storage providers such as Amazon S3, Google Cloud Storage, Cloudinary or Microsoft Azure Blob Storage is often preferred, although the process can seem daunting to you.</p>
<p><strong>Fret not! This article will guide you through integrating with these cloud storage platforms.</strong></p>
<h3 id="heading-outline">Outline</h3>
<ul>
<li><p>Let's Get Started</p>
</li>
<li><p>Prerequisites</p>
</li>
<li><p>Introducing <mark>express-file-wizardry</mark>: Your Ally in File Uploads</p>
</li>
<li><p>Why a New Package?</p>
</li>
<li><p>Setting up Your Express Application</p>
</li>
<li><p>Setting up Preferred Cloud Storage</p>
</li>
<li><p>File Upload Routes and Middleware</p>
</li>
<li><p>Bringing Everything Together and Testing</p>
</li>
<li><p>Advanced Features and Customization</p>
</li>
<li><p>Conclusion</p>
</li>
<li><p>References and Further Reading</p>
</li>
</ul>
<h3 id="heading-lets-get-started">Let's Get Started</h3>
<p>Welcome to my comprehensive guide on mastering file uploads and integrating them seamlessly with various cloud storage platforms in your web applications. Whether you're a junior developer navigating the complexities of file uploads or an experienced developer looking to streamline your workflow, this article is here to assist you every step of the way.</p>
<p>After reading this article, you will have the information and self-assurance to incorporate strong file upload features into your applications.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li><p><strong>Basic knowledge of JavaScript and Node.js</strong> - Familiarity with JavaScript and Node.js is essential to understanding the concepts and code examples in this tutorial.</p>
</li>
<li><p><strong>A Cloud storage platform account</strong> - set up any of the cloud storage <mark>express-file-wizardry</mark> supports</p>
</li>
</ul>
<h3 id="heading-introducing-express-file-wizardry-your-ally-in-file-uploads">Introducing <mark>express-file-wizardry</mark>: Your Ally in File Uploads</h3>
<p><a target="_blank" href="https://www.npmjs.com/package/express-file-wizardry"><mark>express-file-wizardry</mark></a> is a middleware for Express.js that empowers developers to effortlessly handle file uploads in their applications. With its intuitive interface and robust feature set, <mark>express-file-wizardry</mark> abstracts away the complexities of file handling, allowing developers to focus on building innovative solutions without getting bogged down by boilerplate code.</p>
<h3 id="heading-why-a-new-package">Why a new package?</h3>
<p>In a filled environment of options for managing file uploads in Express.js apps, you may question the necessity of a new package. The creation of <mark>express-file-wizardry</mark> came from a specific need that current solutions didn't fully meet.</p>
<ol>
<li><p><strong>Flexible Storage Options</strong>: <mark>express-file-wizardry</mark> seamlessly integrates with a wide range of cloud storage platforms, giving you the freedom to leverage the power of your preferred provider.</p>
</li>
<li><p><strong>Versatile File Type Support</strong>: from popular formats like JPEG, PDF, and MP4 to lesser-known file types, <mark>express-file-wizardry </mark> ensures that your application can handle uploads with ease, regardless of the file format.</p>
</li>
<li><p><strong>Simplified Configuration</strong>: With intuitive and straightforward configuration options, <mark>express-file-wizardry</mark> makes it easy to set up and customize file upload functionality to suit your specific requirements.</p>
</li>
</ol>
<h3 id="heading-step-1-setting-up-your-express-application">STEP 1: Setting up your Express Application</h3>
<ul>
<li><p><strong>Install Node.js and npm</strong><br />  If you haven't already, you'll need to install Node.js and npm (Node Package Manager) on your system. You can download and install them from the official Node.js website: Node.js.</p>
</li>
<li><p><strong>Create a New Directory for Your Project</strong><br />  Open the terminal make a new directory and navigate into the directory for your Express.js project. This can be accomplished by using the following command.</p>
<pre><code class="lang-bash">  mkdir file-upload-app
  <span class="hljs-built_in">cd</span> file-upload-app
</code></pre>
</li>
<li><p><strong>Initialize Your Project</strong><br />  Initialize your project by running:</p>
<pre><code class="lang-bash">  npm init -y
</code></pre>
</li>
<li><p><strong>Install Express</strong><br />  Install Express.js as a dependency for your project:</p>
<pre><code class="lang-bash">  npm install express
</code></pre>
</li>
<li><p><strong>Create Your Express Application</strong><br />  Create a new file, for example, app.js, and write your Express application code:</p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
  <span class="hljs-keyword">const</span> app = express();
  <span class="hljs-keyword">const</span> port = <span class="hljs-number">3000</span>;

  app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    res.send(<span class="hljs-string">'Hello World!'</span>);
  });

  app.listen(port, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${port}</span>`</span>);
  });
</code></pre>
</li>
<li><p><strong>Run Your Express Application</strong><br />  Run your Express application using Node.js:</p>
<pre><code class="lang-bash">  node app.js
</code></pre>
<p>  You should see a message indicating that your server is running on <a target="_blank" href="http://localhost:3000">http://localhost:3000</a>.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709328601812/d37b2914-1588-47ca-a9fa-ecfb77903829.png" alt /></p>
</li>
<li><p><strong>Test Your Application</strong><br />  Open your API client (Postman or any other one) and navigate to <a target="_blank" href="http://localhost:3000">http://localhost:3000</a>. You should see the message "Hello World!" displayed in the response section as shown below.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709328976148/b5960e9a-e2b1-4f1d-8376-ad7901b62eb2.png" alt /></p>
</li>
</ul>
<h3 id="heading-step-2-setting-up-preferred-cloud-storage">STEP 2: Setting up Preferred Cloud Storage</h3>
<ul>
<li><p><strong>Install</strong> <a target="_blank" href="https://www.npmjs.com/package/express-file-wizardry"><mark>express-file-wizardry</mark></a> :</p>
<pre><code class="lang-bash">  npm install express-file-wizardry
</code></pre>
</li>
<li><p><strong>Set a preferred storage type</strong></p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
  <span class="hljs-keyword">const</span> { FileWizardry } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express-file-wizardry'</span>);

  <span class="hljs-keyword">const</span> app = express();
  <span class="hljs-keyword">const</span> port = <span class="hljs-number">3000</span>;

  <span class="hljs-keyword">const</span> fileWizardry = <span class="hljs-keyword">new</span> FileWizardry();
  <span class="hljs-comment">//fileWizardry.setStorageType('memory'); // Storage type can be any of the supported types: memory, disk, cloudinary, amazons3</span>
  fileWizardry.setStorageType(<span class="hljs-string">'cloudinary'</span>, { 
      <span class="hljs-attr">cloud_name</span>: <span class="hljs-string">'my-cloud'</span>, 
      <span class="hljs-attr">api_key</span>: <span class="hljs-string">'api-key'</span>, 
      <span class="hljs-attr">api_secret</span>: <span class="hljs-string">'api-secret'</span> 
  });
  <span class="hljs-comment">// fileWizardry.setStorageType('amazons3', { </span>
      <span class="hljs-comment">// accessKeyId: 'your-access-key', </span>
      <span class="hljs-comment">// secretAccessKey: 'your-secret-key', </span>
      <span class="hljs-comment">// region: 'your-region', </span>
      <span class="hljs-comment">// bucket: 'your-bucket' </span>
  <span class="hljs-comment">// });</span>
  <span class="hljs-comment">// fileWizardry.setStorageType('disk', { </span>
      <span class="hljs-comment">// destination: '/path/to/upload/folder' </span>
  <span class="hljs-comment">// });</span>

  app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    res.send(<span class="hljs-string">'Hello World!'</span>);
  });

  app.listen(port, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${port}</span>`</span>);
  });
</code></pre>
<p>  <strong>Understanding the Code:</strong></p>
<ol>
<li><p><strong>Importing Dependencies</strong>: I start by importing the express framework and the FileWizardry class from the <mark>express-file-wizardry</mark> package.</p>
</li>
<li><p><strong>Initializing Express Application</strong>: Next, I create an instance of the Express application and define the port number (in this case, port 3000) on which the server will listen for incoming requests.</p>
</li>
<li><p><strong>Configuring FileWizardry Storage Type</strong>: The FileWizardry class provides methods for configuring the storage type for file uploads. In this example, we're setting the storage type <code>cloudinary</code> with its configuration options, which indicates that uploaded files will be stored temporarily in memory. Alternatively, you can uncomment one of the other storage type configurations (<code>memory</code>, <code>amazons3</code>, or <code>disk</code>) and provide the necessary configuration options to use Amazon S3, or disk storage, respectively.</p>
</li>
<li><p><strong>Defining Routes</strong>: We define a simple route handler for the root URL <code>/</code> which sends the response 'Hello World!' when the server receives a GET request to this endpoint.</p>
</li>
<li><p><strong>Starting the Server</strong>: Finally, we start the Express server and listen for incoming connections on the specified port. Once the server runs, a message is logged to the console indicating the server's URL.</p>
</li>
</ol>
</li>
</ul>
<h3 id="heading-step-3-file-upload-routes-and-middleware">STEP 3: File Upload Routes and Middleware</h3>
<p>After setting up <mark>express-file-wizardry</mark> to use our preferred cloud storage provider, we will create file upload routes and middleware in our Express.js app. This section will explain how to set routes for managing file uploads and incorporate <mark>express-file-wizardry</mark> middleware for a seamless process.</p>
<ul>
<li><p><strong>Defining File Upload Routes</strong></p>
<pre><code class="lang-javascript">  app.post(<span class="hljs-string">'/upload-signle'</span>, fileWizardry.uploadFile({ <span class="hljs-attr">formats</span>: [<span class="hljs-string">'image/jpeg'</span>, <span class="hljs-string">'image/png'</span>], <span class="hljs-attr">fieldName</span>: <span class="hljs-string">'image'</span> }), <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    <span class="hljs-comment">// File upload successful</span>
    res.status(<span class="hljs-number">200</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'File uploaded successfully'</span>, <span class="hljs-attr">file</span>: req.file });
  });

  app.post(<span class="hljs-string">'/upload-multi'</span>, fileWizardry.uploadFile({ <span class="hljs-attr">formats</span>: [<span class="hljs-string">'image/jpeg'</span>, <span class="hljs-string">'image/png'</span>], <span class="hljs-attr">fieldName</span>: <span class="hljs-string">'images'</span>,  <span class="hljs-attr">multiFile</span>: <span class="hljs-literal">true</span> }), <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    <span class="hljs-comment">// File upload successful</span>
    res.status(<span class="hljs-number">200</span>).json({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Files uploaded successfully'</span>, <span class="hljs-attr">file</span>: req.files });
  });
</code></pre>
<p>  In this example:</p>
<ul>
<li><p>I defined a <code>POST</code> route <code>/upload-single</code> to handle single file upload using the <code>fileWizardry.uploadFile</code> middleware. This middleware expects a single file with the field name <code>image</code>. This middleware takes an options object specifying the allowed file formats (<code>formats</code>) and the field name (<code>fieldName</code>) under which the file will be uploaded.</p>
</li>
<li><p>In contrast, the second route <code>/upload-multi</code> is designed to handle multiple file uploads. Here, the <code>fileWizardry.uploadFile</code> middleware is again employed, but with an added option: <code>multiFile</code>, which is set to <code>true</code>. This indicates that the route can process multiple files simultaneously.</p>
</li>
<li><p>Across both routes, I've restricted uploads to only accept JPEG and PNG image files, ensuring that the application remains focused on handling image uploads exclusively.</p>
</li>
</ul>
</li>
</ul>
    <div data-node-type="callout">
    <div data-node-type="callout-emoji">💡</div>
    <div data-node-type="callout-text"><strong>Note - </strong>Files successfully uploaded as multiple are accessible from the <code>req.files</code> and <code>req.file</code> otherwise</div>
    </div>

<ul>
<li><p><strong>Error Handling and Validation</strong></p>
<pre><code class="lang-javascript">  app.post(<span class="hljs-string">'/upload-single'</span>, fileWizardry.uploadFile({ <span class="hljs-attr">formats</span>: [<span class="hljs-string">'image/jpeg'</span>, <span class="hljs-string">'image/png'</span>], <span class="hljs-attr">fieldName</span>: <span class="hljs-string">'image'</span> }), <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (req.fileValidationError) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ 
          <span class="hljs-attr">error</span>: req.fileValidationError.message 
        });
    }
    res.status(<span class="hljs-number">200</span>).json({ 
        <span class="hljs-attr">message</span>: <span class="hljs-string">'File uploaded successfully'</span>, 
        <span class="hljs-attr">file</span>: req.file 
    });
  });

  app.post(<span class="hljs-string">'/upload-multi'</span>, fileWizardry.uploadFile({ <span class="hljs-attr">formats</span>: [<span class="hljs-string">'image/jpeg'</span>, <span class="hljs-string">'image/png'</span>], <span class="hljs-attr">fieldName</span>: <span class="hljs-string">'images'</span>,  <span class="hljs-attr">multiFile</span>: <span class="hljs-literal">true</span> }), <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (req.fileValidationError) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ 
          <span class="hljs-attr">error</span>: req.fileValidationError.message 
        });
    }
    res.status(<span class="hljs-number">200</span>).json({ 
        <span class="hljs-attr">message</span>: <span class="hljs-string">'Files uploaded successfully'</span>, 
        <span class="hljs-attr">file</span>: req.files
    });
  });
</code></pre>
<ul>
<li><p><strong>Validation Check</strong>: After the file upload middleware, a validation check is performed using <code>req.fileValidationError</code>. If a validation error occurs during the file upload process, <code>req.fileValidationError</code> will be populated with details of the error. Every validation has been handled by the <mark>express-file-wizardry</mark> package.</p>
</li>
<li><p><strong>Error Handling</strong>: If <code>req.fileValidationError</code> exists, indicating a validation error, the route sends a 400 Bad Request response with an error message describing the validation error.</p>
</li>
<li><p><strong>Success Response</strong>: If no validation errors occur, the route sends a 200 OK response with a success message and information about the uploaded file(s) <code>req.file</code> or <code>req.files</code></p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-advanced-features-and-customization">Advanced Features and Customization</h3>
<ul>
<li><p><strong>Max Size Upload Option (Optional)</strong><br />  Maximum allowed size for each uploaded file, in bytes. If specified, files exceeding this size will be rejected.</p>
</li>
<li><p><strong>File Deletion</strong><br />  File deletion is an essential aspect of managing uploaded files, especially when dealing with storage solutions like local disk or cloud storage. Here's how you can implement file deletion in an Express.js application using <mark>express-file-wizardry</mark></p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">const</span> fileWizardry = <span class="hljs-keyword">new</span> FileWizardry();

  fileWizardry.deleteFile(<span class="hljs-string">'cloudinary'</span>, {
      <span class="hljs-attr">public_id</span>: <span class="hljs-string">'cloudinary_file_public_Id'</span>,
  });

  fileWizardry.deleteFile(<span class="hljs-string">'amazons3'</span>, {
      <span class="hljs-attr">key</span>: <span class="hljs-string">'my-file'</span>,
      <span class="hljs-attr">Bucket</span>: <span class="hljs-string">'my-bucket'</span>,
  });

  fileWizardry.deleteFile(<span class="hljs-string">'disk'</span>, {
      <span class="hljs-attr">path</span>: <span class="hljs-string">'my-file'</span>,
  });
</code></pre>
</li>
</ul>
<h3 id="heading-bringing-everything-together-and-testing">Bringing Everything Together and Testing</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { config } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'dotenv'</span>);
<span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> { FileWizardry } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express-file-wizardry'</span>);

config();
<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> port = <span class="hljs-number">3000</span>;

<span class="hljs-keyword">const</span> fileWizardry = <span class="hljs-keyword">new</span> FileWizardry();
fileWizardry.setStorageType(<span class="hljs-string">'cloudinary'</span>, {
    <span class="hljs-attr">cloud_name</span>: process.env.CLOUD_NAME,
    <span class="hljs-attr">api_key</span>: process.env.API_KEY,
    <span class="hljs-attr">api_secret</span>: process.env.API_SECRET,
});

app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    res.send(<span class="hljs-string">'Hello World!'</span>);
});

app.post(
    <span class="hljs-string">'/upload-single'</span>,
    fileWizardry.upload({ <span class="hljs-attr">formats</span>: [<span class="hljs-string">'image/jpeg'</span>, <span class="hljs-string">'image/png'</span>], <span class="hljs-attr">fieldName</span>: <span class="hljs-string">'image'</span> }),
    <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">if</span> (req.fileValidationError) {
                <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({
                    <span class="hljs-attr">error</span>: req.fileValidationError.message || req.fileValidationError,
                });
            }
            res.status(<span class="hljs-number">200</span>).json({
                <span class="hljs-attr">message</span>: <span class="hljs-string">'File uploaded successfully'</span>,
                <span class="hljs-attr">file</span>: req.file,
            });
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-built_in">console</span>.log(error);
            res.status(<span class="hljs-number">500</span>).json({
                <span class="hljs-attr">message</span>: <span class="hljs-string">'Internal server error'</span>,
                <span class="hljs-attr">error</span>: error.message,
            });
        }
    }
);

app.post(
    <span class="hljs-string">'/upload-multi'</span>,
    fileWizardry.upload({
        <span class="hljs-attr">formats</span>: [<span class="hljs-string">'image/jpeg'</span>, <span class="hljs-string">'image/png'</span>],
        <span class="hljs-attr">fieldName</span>: <span class="hljs-string">'images'</span>,
        <span class="hljs-attr">multiFile</span>: <span class="hljs-literal">true</span>,
    }),
    <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">if</span> (req.fileValidationError) {
                <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({
                    <span class="hljs-attr">error</span>: req.fileValidationError.message || req.fileValidationError,
                });
            }
            res.status(<span class="hljs-number">200</span>).json({
                <span class="hljs-attr">message</span>: <span class="hljs-string">'Files uploaded successfully'</span>,
                <span class="hljs-attr">file</span>: req.files,
            });
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-built_in">console</span>.log(error);
            res.status(<span class="hljs-number">500</span>).json({
                <span class="hljs-attr">message</span>: <span class="hljs-string">'Internal server error'</span>,
                <span class="hljs-attr">error</span>: error.message,
            });
        }
    }
);

app.delete(<span class="hljs-string">'/delete-file/:fileId'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">await</span> fileWizardry.deleteFile(req.params.fileId);

        res.status(<span class="hljs-number">200</span>).json({
            <span class="hljs-attr">message</span>: <span class="hljs-string">'File uploaded successfully'</span>,
            <span class="hljs-attr">file</span>: req.file,
        });
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.log(error);
        res.status(<span class="hljs-number">500</span>).json({
            <span class="hljs-attr">message</span>: <span class="hljs-string">'Internal server error'</span>,
            <span class="hljs-attr">error</span>: error.message,
        });
    }
});

app.listen(port, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Security Note - </strong>It is recommended to store sensitive information such as <code>API keys</code>, <code>credentials</code>, and other configuration variables in environment variables. You can use packages like <code>dotenv</code> to load environment variables from a <code>.env</code> file. Ensure that the <code>.env</code> file is added to the <code>.gitignore</code> file to prevent sensitive information from being exposed in version control.</div>
</div>

<p>Start your Express.js application by running the following command in your terminal:</p>
<pre><code class="lang-javascript">node app.js
</code></pre>
<ul>
<li><p>Once the application is running, you can now test your application by sending requests to the defined routes (e.g., <code>/upload-single</code> for single file upload, <code>/upload-multi</code> for multiple file uploads and <code>/delete-file/:fileId</code> for file deletion) using your preferred API client.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709378667771/e5777ac1-c5e9-418f-9049-fde3b956b590.png" alt="No file uploaded" class="image--center mx-auto" /></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709378763580/9f6dd77c-455f-41ba-9488-0b2e5438bacc.png" alt="Invalid file format" class="image--center mx-auto" /></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709379194453/c86d0a60-6dd3-42a5-9803-bb0b0fdcf433.png" alt="Single upload successful" class="image--center mx-auto" /></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709379338891/c683db92-03b1-438c-833a-5b401b8a072a.png" alt="Multiple upload successful" class="image--center mx-auto" /></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709380724173/7b17afdd-56ef-4ad1-8003-c1bc2e26e7ca.png" alt="Delete successful" class="image--center mx-auto" /></p>
</li>
<li><p>Monitor the terminal for any logs or errors that may occur while your application is running. If there are any issues, you can debug them based on the error messages displayed.</p>
</li>
</ul>
<p>You can <a target="_blank" href="https://github.com/devfresher/file-upload-app-with-express-file-wizardry">click here to check</a> out the complete code on GitHub.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>This article delves into the integration of file upload features in Express.js using express-file-wizardry, addressing the importance of this function in web development, challenges, and the tool's benefits. The guide covers setting up an Express.js app for file uploads, configuration of cloud storage options, defining routes, error handling, security practices, and advanced customization. express-file-wizardry streamlines the file upload process, boosting performance and user experience. It supports local disk, cloud storage like Amazon S3, and third-party services, offering a comprehensive solution. Overall, it enables developers to focus on creating robust apps without the hassle of managing file uploads.</p>
<p>Happy Coding!</p>
<h3 id="heading-references-and-further-reading">References and Further Reading</h3>
<ul>
<li><p><a target="_blank" href="https://www.npmjs.com/package/express-file-wizardry">Express-file-wizardry Docs</a></p>
</li>
<li><p><a target="_blank" href="https://www.npmjs.com/package/dotenv">dotenv Docs</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>