<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tope Taiwo</title>
    <description>The latest articles on DEV Community by Tope Taiwo (@topzyray).</description>
    <link>https://dev.to/topzyray</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1090540%2F590ad3a6-3119-403e-a42e-3620199cff1d.jpeg</url>
      <title>DEV Community: Tope Taiwo</title>
      <link>https://dev.to/topzyray</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/topzyray"/>
    <language>en</language>
    <item>
      <title>Payments Are a Workflow, Not Just an API Call</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:54:59 +0000</pubDate>
      <link>https://dev.to/topzyray/payments-are-a-workflow-not-just-an-api-call-1bm8</link>
      <guid>https://dev.to/topzyray/payments-are-a-workflow-not-just-an-api-call-1bm8</guid>
      <description>&lt;p&gt;&lt;em&gt;"Design payment processing as a recoverable workflow with verified outcomes, idempotency, separate financial records, and reconciliation."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A payment integration can look deceptively simple.&lt;/p&gt;

&lt;p&gt;Call a payment provider.&lt;/p&gt;

&lt;p&gt;Receive a response.&lt;/p&gt;

&lt;p&gt;Mark the transaction successful.&lt;/p&gt;

&lt;p&gt;Real payment systems are more complicated.&lt;/p&gt;

&lt;p&gt;A payment involves external infrastructure, asynchronous communication, financial records, and business rules that must remain consistent even when something fails.&lt;/p&gt;

&lt;p&gt;A request can time out after the provider has processed it. A webhook can arrive more than once. A customer can close the browser before the checkout process finishes. A payment can be completed externally while the application is temporarily unavailable.&lt;/p&gt;

&lt;p&gt;These are not exceptional cases that can simply be ignored. They are normal conditions in distributed systems.&lt;/p&gt;

&lt;p&gt;The challenge is not just initiating a payment. It is determining what happened, recording the outcome reliably, and ensuring that the rest of the application responds correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A payment is a workflow
&lt;/h2&gt;

&lt;p&gt;A better mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initiated
   │
   ▼
Pending
   │
   ├── Successful
   ├── Failed
   └── Abandoned
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system needs to model those states.&lt;/p&gt;

&lt;p&gt;In practice, the lifecycle may require additional states, such as &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;processing&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt;, &lt;code&gt;cancelled&lt;/code&gt;, &lt;code&gt;reversed&lt;/code&gt;, or &lt;code&gt;refunded&lt;/code&gt;, depending on the provider and business requirements.&lt;/p&gt;

&lt;p&gt;It is also important to distinguish a payment attempt from an order or purchase.&lt;/p&gt;

&lt;p&gt;An order represents what a customer intends to buy. A payment attempt represents an attempt to collect the money for that order. One order may have several payment attempts if an earlier attempt fails or expires.&lt;/p&gt;

&lt;p&gt;Treating these as separate concepts helps prevent retries from creating duplicate purchases or losing the relationship between the customer's intention and the financial transaction.&lt;/p&gt;

&lt;p&gt;A simplified domain model might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PaymentStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;successful&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abandoned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;providerReference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amountMinor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is illustrative rather than a complete payment schema. A production implementation would also need to define valid state transitions, timestamps, provider identifiers, and any relevant refund or reconciliation records.&lt;/p&gt;

&lt;p&gt;The key point is that payment status should represent a verified business fact, not merely the outcome of one HTTP request.&lt;/p&gt;

&lt;p&gt;State transitions should also be controlled. A delayed failure notification, for example, must not blindly overwrite a payment that has already been verified as successful. Similarly, a refund should not be represented as if the original payment never occurred.&lt;/p&gt;

&lt;p&gt;Financial workflows need explicit rules for how states change and which evidence permits each transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the API response is not the entire story
&lt;/h2&gt;

&lt;p&gt;External payment providers can communicate through callbacks or webhooks.&lt;/p&gt;

&lt;p&gt;That means payment processing is distributed.&lt;/p&gt;

&lt;p&gt;Our application and the payment provider are separate systems.&lt;/p&gt;

&lt;p&gt;Neither system can assume that every message will arrive immediately, exactly once, or while the other system is available.&lt;/p&gt;

&lt;p&gt;Consider a customer who initiates checkout. The provider processes the payment, but the response to our backend times out before the application receives confirmation.&lt;/p&gt;

&lt;p&gt;The application now has an uncertain outcome.&lt;/p&gt;

&lt;p&gt;If it immediately marks the payment as failed and starts another payment attempt, the customer could eventually be charged twice.&lt;/p&gt;

&lt;p&gt;If it marks the payment as successful merely because the checkout request was accepted, the customer could receive a product without a completed payment.&lt;/p&gt;

&lt;p&gt;The backend therefore needs to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate notifications;&lt;/li&gt;
&lt;li&gt;delayed notifications;&lt;/li&gt;
&lt;li&gt;verification;&lt;/li&gt;
&lt;li&gt;inconsistent intermediate states.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should also handle temporary provider outages, request timeouts, malformed messages, and events that arrive in an unexpected order.&lt;/p&gt;

&lt;p&gt;A reliable design separates payment initiation from payment confirmation.&lt;/p&gt;

&lt;p&gt;Initiating a payment means that the application has requested a payment operation. It does not necessarily mean that money has been received.&lt;/p&gt;

&lt;p&gt;Similarly, redirecting a customer to a success page is not proof that the payment succeeded. The browser is controlled by the customer and cannot serve as the sole authority for a financial decision.&lt;/p&gt;

&lt;p&gt;The backend needs a trusted way to establish the final payment outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify before granting access
&lt;/h2&gt;

&lt;p&gt;For a provider such as Paystack, the backend should treat the provider's verified transaction status as evidence for deciding whether to complete the business operation.&lt;/p&gt;

&lt;p&gt;The exact verification process depends on the provider's integration model, but it commonly involves verifying the transaction through a trusted server-side API or validating an authenticated webhook and checking the relevant transaction details.&lt;/p&gt;

&lt;p&gt;The application should confirm that the transaction matches its own expected values, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the provider reference;&lt;/li&gt;
&lt;li&gt;the expected amount;&lt;/li&gt;
&lt;li&gt;the currency;&lt;/li&gt;
&lt;li&gt;the associated order or purchase;&lt;/li&gt;
&lt;li&gt;the verified payment status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The amount and currency are especially important. A successful transaction is not sufficient if it does not correspond to the amount and currency expected for the purchase.&lt;/p&gt;

&lt;p&gt;For a webhook, signature verification must be performed according to the provider's documented requirements, using the unmodified request body where required. Invalid signatures should be rejected. Secrets should remain on the server and must not be exposed to the frontend.&lt;/p&gt;

&lt;p&gt;A typical flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer
   │
   ▼
Backend creates payment attempt
   │
   ▼
Payment provider
   │
   ├── Checkout response
   │
   └── Webhook / verification
              │
              ▼
       Verify authenticity
              │
              ▼
       Verify transaction details
              │
              ▼
       Record payment outcome
              │
              ▼
       Apply business effects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser redirect and webhook can both trigger a verification attempt, but neither should cause the application to bypass its own payment-state rules.&lt;/p&gt;

&lt;p&gt;A useful principle is to separate the evidence received from the provider from the business decision made by the application.&lt;/p&gt;

&lt;p&gt;The provider reports a transaction outcome. The application determines whether that outcome satisfies the conditions required to fulfil the associated order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency matters
&lt;/h2&gt;

&lt;p&gt;If the same payment notification arrives more than once, processing it twice should not create two successful purchases.&lt;/p&gt;

&lt;p&gt;The application needs a stable transaction identity and idempotent processing.&lt;/p&gt;

&lt;p&gt;Duplicate delivery is a normal possibility in webhook-based systems. Providers may retry notifications when delivery fails or when they do not receive the expected acknowledgement.&lt;/p&gt;

&lt;p&gt;A webhook handler should therefore be safe to execute more than once for the same event.&lt;/p&gt;

&lt;p&gt;For example, the application can use the provider's transaction reference or event identifier to identify repeated processing attempts. Database uniqueness constraints can enforce important parts of that guarantee.&lt;/p&gt;

&lt;p&gt;A simplified table might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;provider_reference&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_minor&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="nb"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This illustrates how a unique provider reference can prevent duplicate payment records for the same reference. A production schema should also enforce valid statuses and appropriate relationships, and should define whether a provider reference is unique globally or within a provider account.&lt;/p&gt;

&lt;p&gt;However, idempotency is not simply checking whether a row already exists.&lt;/p&gt;

&lt;p&gt;The application must also handle concurrent processing. Two webhook deliveries could arrive close together, both observe a pending payment, and both attempt to apply the same business effect.&lt;/p&gt;

&lt;p&gt;A robust implementation uses database transactions, appropriate uniqueness constraints, conditional state transitions, or row-level locking where necessary to prevent duplicate effects.&lt;/p&gt;

&lt;p&gt;For example, marking an order as paid and recording the corresponding payment result should be coordinated so that a crash does not leave the system with a successful payment but no durable record of the resulting business decision.&lt;/p&gt;

&lt;p&gt;Idempotency must extend beyond the payment row itself. If successful payment grants an examination subscription, creates an entitlement, or records a referral commission, repeating the payment handler must not grant that entitlement or commission twice.&lt;/p&gt;

&lt;p&gt;The desired behavior is not necessarily that the handler runs only once.&lt;/p&gt;

&lt;p&gt;It is that repeated processing produces the same valid business outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep financial records separate from business effects
&lt;/h2&gt;

&lt;p&gt;A successful payment can trigger several operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;activating a subscription;&lt;/li&gt;
&lt;li&gt;granting access to an examination product;&lt;/li&gt;
&lt;li&gt;confirming an order;&lt;/li&gt;
&lt;li&gt;recording a referral commission;&lt;/li&gt;
&lt;li&gt;scheduling a payout;&lt;/li&gt;
&lt;li&gt;sending a receipt or notification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These operations are related, but they are not the same thing.&lt;/p&gt;

&lt;p&gt;The payment record captures the financial outcome. The entitlement or subscription records capture what the customer is allowed to use. Referral records capture commission eligibility and amounts. Payout records capture the movement of funds to a beneficiary.&lt;/p&gt;

&lt;p&gt;Keeping these responsibilities distinct makes the system easier to audit and recover.&lt;/p&gt;

&lt;p&gt;For example, if a payment is confirmed but subscription activation fails, the application should not lose the payment result simply because a downstream operation encountered an error.&lt;/p&gt;

&lt;p&gt;The system needs a way to retry the failed business effect without charging the customer again or pretending that the payment itself failed.&lt;/p&gt;

&lt;p&gt;This is one reason asynchronous processing and transactional outbox patterns can be useful.&lt;/p&gt;

&lt;p&gt;The application can persist the verified payment outcome and the intent to perform downstream work in one database transaction. A worker can then process that work, using idempotent operations to handle retries safely.&lt;/p&gt;

&lt;p&gt;This does not make every operation atomic across every system. It creates a durable path for coordinating work across system boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payment architecture
&lt;/h2&gt;

&lt;p&gt;For systems such as the education products I have worked on, payment state can affect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;access;&lt;/li&gt;
&lt;li&gt;subscriptions;&lt;/li&gt;
&lt;li&gt;examination products;&lt;/li&gt;
&lt;li&gt;referrals;&lt;/li&gt;
&lt;li&gt;payouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means payment processing should not be treated as a small utility function.&lt;/p&gt;

&lt;p&gt;It is part of the business workflow.&lt;/p&gt;

&lt;p&gt;A practical architecture might separate responsibilities into components such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payments Module
   │
   ├── Payment Initiation
   ├── Provider Integration
   ├── Webhook Verification
   ├── Payment State Management
   ├── Reconciliation
   └── Refund Handling
           │
           ▼
    Business Workflows
           │
           ├── Entitlements
           ├── Subscriptions
           ├── Referrals
           └── Payouts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact module boundaries depend on the application, but the separation helps prevent provider-specific behavior from spreading throughout the business domain.&lt;/p&gt;

&lt;p&gt;For example, a subscription module should not need to know how a specific provider signs webhook requests. It should respond to a trusted payment outcome or a clearly defined application event.&lt;/p&gt;

&lt;p&gt;Similarly, the payment module should not assume that recording a successful payment means every downstream operation has already completed.&lt;/p&gt;

&lt;p&gt;This separation also makes it easier to support another payment provider in the future without rewriting the business rules for subscriptions, entitlements, or referrals.&lt;/p&gt;

&lt;p&gt;The objective is not to abstract every provider detail prematurely. It is to prevent external payment infrastructure from becoming inseparable from the application's business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation is part of payment engineering
&lt;/h2&gt;

&lt;p&gt;Webhooks and retries improve reliability, but they do not eliminate uncertainty.&lt;/p&gt;

&lt;p&gt;A webhook might never be processed successfully. A temporary outage might leave a payment in a pending state. A local database update might fail after the provider has completed the transaction.&lt;/p&gt;

&lt;p&gt;A payment system therefore needs a reconciliation process.&lt;/p&gt;

&lt;p&gt;Reconciliation compares the application's payment records with the provider's transaction records and identifies discrepancies that require investigation or correction.&lt;/p&gt;

&lt;p&gt;For example, a scheduled task might examine transactions that have remained pending beyond an expected period, query the provider for their current status, and update the local state when the result has been verified.&lt;/p&gt;

&lt;p&gt;The process should be designed carefully. It should not blindly overwrite local records or repeat business effects without applying the same verification and idempotency rules used by webhook processing.&lt;/p&gt;

&lt;p&gt;Useful operational metrics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the number and age of pending transactions;&lt;/li&gt;
&lt;li&gt;webhook delivery and processing failures;&lt;/li&gt;
&lt;li&gt;verification failures;&lt;/li&gt;
&lt;li&gt;duplicate events;&lt;/li&gt;
&lt;li&gt;payment-to-entitlement delays;&lt;/li&gt;
&lt;li&gt;discrepancies discovered during reconciliation;&lt;/li&gt;
&lt;li&gt;refund and payout failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These measurements help distinguish a payment provider problem from an application processing problem.&lt;/p&gt;

&lt;p&gt;They also make it easier to detect situations where the financial transaction succeeded but the customer has not received the expected product or service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;A payment provider gives us infrastructure.&lt;/p&gt;

&lt;p&gt;The application still needs to model its own business state.&lt;/p&gt;

&lt;p&gt;It must distinguish initiation from confirmation, verify the transaction, handle duplicate and delayed messages, protect against repeated business effects, and recover when part of the workflow fails.&lt;/p&gt;

&lt;p&gt;That is why payment engineering is fundamentally a workflow-design problem.&lt;/p&gt;

&lt;p&gt;The goal is not simply to process a successful payment.&lt;/p&gt;

&lt;p&gt;It is to ensure that the payment, the financial record, and the business outcome remain consistent—even when the systems involved do not behave as expected.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backenddevelopment</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Multi-Tenancy: What Changes When One Application Serves Many Organizations</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:11:58 +0000</pubDate>
      <link>https://dev.to/topzyray/multi-tenancy-what-changes-when-one-application-serves-many-organizations-38m1</link>
      <guid>https://dev.to/topzyray/multi-tenancy-what-changes-when-one-application-serves-many-organizations-38m1</guid>
      <description>&lt;p&gt;&lt;em&gt;"Design multi-tenant applications with deliberate data isolation, trusted tenant context, tenant-aware authorization, and boundary testing."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Building software for one organization is different from building software that serves many organizations.&lt;/p&gt;

&lt;p&gt;This distinction has become particularly relevant to me through work around school-management software.&lt;/p&gt;

&lt;p&gt;When an application serves a single organization, many assumptions can remain implicit. Users belong to the same organizational environment. Configuration is relatively straightforward. Data access often assumes a single administrative boundary.&lt;/p&gt;

&lt;p&gt;Those assumptions become dangerous when the same application serves multiple organizations.&lt;/p&gt;

&lt;p&gt;A school-management platform, for example, may serve hundreds of schools, each with its own students, teachers, classes, academic sessions, billing arrangements, and administrative policies.&lt;/p&gt;

&lt;p&gt;The application may share infrastructure, but the organizations using it must remain distinct in the ways that matter to the business.&lt;/p&gt;

&lt;p&gt;That is where multi-tenancy becomes an architectural concern rather than just a database design decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is multi-tenancy?
&lt;/h2&gt;

&lt;p&gt;A multi-tenant application serves multiple organizations while sharing the application infrastructure.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
   │
   ├── School A
   ├── School B
   └── School C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The challenge is ensuring that each tenant's data remains correctly isolated.&lt;/p&gt;

&lt;p&gt;Sharing infrastructure can reduce operational overhead and make it easier to deploy, monitor, and maintain a common application. It can also allow improvements to be delivered consistently across organizations.&lt;/p&gt;

&lt;p&gt;However, sharing an application does not mean sharing unrestricted access to its data.&lt;/p&gt;

&lt;p&gt;Each tenant may have its own users, roles, configuration, records, and business rules. Some capabilities may be common to all tenants, while others may depend on the organization's subscription, policies, or enabled features.&lt;/p&gt;

&lt;p&gt;A multi-tenant architecture must support these differences without allowing one organization's operations to interfere improperly with another's.&lt;/p&gt;

&lt;p&gt;There are several common approaches to tenant isolation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared database and shared tables:&lt;/strong&gt; Multiple tenants use the same tables, with a tenant identifier distinguishing their records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared database with separate schemas:&lt;/strong&gt; Tenants share a database server but use separate database schemas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate databases:&lt;/strong&gt; Each tenant has a dedicated database, providing a stronger separation boundary at the database level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These approaches involve different trade-offs in cost, operational complexity, isolation, backup and restoration, and tenant-specific customization.&lt;/p&gt;

&lt;p&gt;A shared database with tenant-scoped tables is often a practical starting point for a SaaS application. It is not automatically the right choice for every system, particularly where contractual, regulatory, or operational requirements demand stronger isolation.&lt;/p&gt;

&lt;p&gt;The important point is to choose an isolation strategy deliberately rather than allowing it to emerge accidentally from the initial schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tenant context
&lt;/h2&gt;

&lt;p&gt;A request should carry enough information for the application to determine its tenant context.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
 │
 ▼
Identity
 │
 ▼
Tenant
 │
 ▼
Authorization
 │
 ▼
Business Operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tenant context should not be treated as an optional detail.&lt;/p&gt;

&lt;p&gt;It affects authorization and data access.&lt;/p&gt;

&lt;p&gt;A user may belong to several organizations, so identifying the authenticated user is not always sufficient to determine which organization's resources the user is permitted to access.&lt;/p&gt;

&lt;p&gt;For example, a teacher may work at two schools. A school administrator may manage one school, while a platform administrator may have a separate, explicitly authorized level of access.&lt;/p&gt;

&lt;p&gt;The application needs to establish the active organizational context and verify that the authenticated identity is permitted to operate within it.&lt;/p&gt;

&lt;p&gt;A request might include a tenant identifier in its route or headers, or the application might resolve the tenant from a verified domain or subdomain. Regardless of the mechanism, the supplied identifier should be treated as a request to select a context—not as proof of authorization.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TenantContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;membershipId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an illustrative representation of the context established after authentication and membership verification. It should not be constructed by blindly trusting client-supplied role values or tenant identifiers.&lt;/p&gt;

&lt;p&gt;A reliable request flow should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticate the identity.&lt;/li&gt;
&lt;li&gt;Resolve the requested tenant.&lt;/li&gt;
&lt;li&gt;Verify that the identity has an active membership or other explicitly authorized access to that tenant.&lt;/li&gt;
&lt;li&gt;Establish the tenant context for the operation.&lt;/li&gt;
&lt;li&gt;Enforce authorization and tenant-scoped data access.&lt;/li&gt;
&lt;li&gt;Record relevant actions in the audit trail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This context should be propagated consistently through the application layers that need it.&lt;/p&gt;

&lt;p&gt;If controllers enforce tenant boundaries but repositories can execute unrestricted queries, the system still has a gap. If background jobs process tenant-specific records without carrying their tenant context, the same problem can reappear outside the HTTP request lifecycle.&lt;/p&gt;

&lt;p&gt;Tenant context is therefore a cross-cutting application concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data isolation
&lt;/h2&gt;

&lt;p&gt;A multi-tenant database design needs a clear strategy for separating tenant data.&lt;/p&gt;

&lt;p&gt;A shared database can still provide logical tenant isolation, provided every relevant operation correctly applies tenant boundaries.&lt;/p&gt;

&lt;p&gt;The dangerous situation is one where a developer can accidentally query data without tenant context.&lt;/p&gt;

&lt;p&gt;Consider a simplified PostgreSQL table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tenant_id&lt;/code&gt; column identifies the organization that owns each record.&lt;/p&gt;

&lt;p&gt;A tenant-scoped query should use that boundary explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value supplied to &lt;code&gt;$1&lt;/code&gt; must come from the application's verified tenant context, not from an untrusted assumption about the requester.&lt;/p&gt;

&lt;p&gt;The column alone does not guarantee isolation. A query that forgets the tenant predicate can expose records belonging to other organizations.&lt;/p&gt;

&lt;p&gt;This is why tenant isolation should be reinforced at multiple levels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At the application layer&lt;/strong&gt;, repositories and data-access functions should require tenant context for tenant-owned operations. Avoid APIs that make unrestricted access the easiest default.&lt;/p&gt;

&lt;p&gt;For example, a repository method that accepts a tenant identifier makes the ownership boundary explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StudentRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;studentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Student&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation must apply both identifiers to the query. Merely adding &lt;code&gt;tenantId&lt;/code&gt; to a method signature does not enforce isolation if the implementation ignores it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At the database layer&lt;/strong&gt;, PostgreSQL Row-Level Security (RLS) can provide an additional enforcement mechanism. Policies can restrict which rows a database role may read or modify based on a tenant context established for the database transaction or connection.&lt;/p&gt;

&lt;p&gt;RLS must be configured carefully. The application role, table ownership, policy definitions, and connection-pool behavior all matter. For example, table owners and roles with certain privileges can bypass policies unless the design explicitly accounts for that behavior. A session-level tenant setting must also not leak between pooled connections or transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At the schema level&lt;/strong&gt;, constraints should prevent invalid relationships. If a student belongs to one tenant, a relationship between that student and another tenant's class should not be accepted merely because both record identifiers exist.&lt;/p&gt;

&lt;p&gt;Composite keys or unique constraints involving &lt;code&gt;tenant_id&lt;/code&gt;, together with appropriate foreign-key design, can help enforce these ownership relationships.&lt;/p&gt;

&lt;p&gt;The objective is to make tenant isolation an invariant of the system rather than a convention that every developer must remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorization must include tenant context
&lt;/h2&gt;

&lt;p&gt;Authentication answers the question: who is making the request?&lt;/p&gt;

&lt;p&gt;Authorization answers: what is that identity allowed to do?&lt;/p&gt;

&lt;p&gt;Multi-tenancy adds another important question: within which organizational context is the action permitted?&lt;/p&gt;

&lt;p&gt;A user may be authorized to view student records in School A but have no permission to view the same category of records in School B.&lt;/p&gt;

&lt;p&gt;Even when both schools use the same application and database, their permissions must remain distinct.&lt;/p&gt;

&lt;p&gt;This is particularly important for operations involving approvals, financial records, personnel information, and administrative configuration.&lt;/p&gt;

&lt;p&gt;A useful authorization decision may depend on several factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the authenticated identity;&lt;/li&gt;
&lt;li&gt;the active tenant;&lt;/li&gt;
&lt;li&gt;the user's membership in that tenant;&lt;/li&gt;
&lt;li&gt;the role or permissions assigned within that membership;&lt;/li&gt;
&lt;li&gt;the resource being accessed;&lt;/li&gt;
&lt;li&gt;the action being performed;&lt;/li&gt;
&lt;li&gt;any applicable business rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A global role such as &lt;code&gt;admin&lt;/code&gt; is often too broad to express these distinctions safely.&lt;/p&gt;

&lt;p&gt;The application should evaluate permissions in the context of the tenant and the requested operation. Tenant-specific authorization must also apply to resource identifiers: knowing a valid student ID, invoice ID, or requisition ID should not grant access to a resource belonging to another tenant.&lt;/p&gt;

&lt;p&gt;This is one reason authorization should not be implemented solely by hiding buttons or links in the frontend. The backend must enforce the actual access rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roles can also be tenant-specific
&lt;/h2&gt;

&lt;p&gt;A user may have a different role in different organizational contexts.&lt;/p&gt;

&lt;p&gt;That means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ├── Tenant A → Role X
 └── Tenant B → Role Y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a user might be an administrator at one school and a teacher at another.&lt;/p&gt;

&lt;p&gt;Representing these permissions as one global role would create ambiguity. Should the user receive administrator privileges everywhere, or only within the school where those privileges were granted?&lt;/p&gt;

&lt;p&gt;A tenant-aware membership model makes the distinction explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
  │
  ├── Membership A
  │      ├── Tenant A
  │      └── Role: School Administrator
  │
  └── Membership B
         ├── Tenant B
         └── Role: Teacher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified relational design might include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 └── TenantMembership
        ├── userId
        ├── tenantId
        └── status

TenantMembership
 └── MembershipRole
        ├── membershipId
        └── roleId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this model, the membership establishes the user's relationship with a tenant, and roles are assigned within that relationship.&lt;/p&gt;

&lt;p&gt;The design can be extended to support permissions, invitations, membership expiry, and role changes without treating the user's identity as equivalent to their authority in every organization.&lt;/p&gt;

&lt;p&gt;Platform-level administration can still exist, but it should be represented as a separate, deliberate authorization path rather than an accidental consequence of tenant membership.&lt;/p&gt;

&lt;p&gt;The key is to distinguish the identity itself from the organizational permissions associated with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tenant isolation extends beyond database queries
&lt;/h2&gt;

&lt;p&gt;Database queries are an important part of multi-tenancy, but they are not the only place where tenant boundaries can fail.&lt;/p&gt;

&lt;p&gt;Consider the other components of a typical SaaS application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching:&lt;/strong&gt; A cache key that includes a resource ID but omits the tenant context may return one organization's data to another organization. Tenant-owned cache entries should include the relevant tenant scope where appropriate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background jobs:&lt;/strong&gt; A queued job should retain the tenant identity required to process its work. A worker must not assume that the tenant context from an earlier HTTP request is still available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File storage:&lt;/strong&gt; Uploaded documents, profile images, reports, and exports need tenant-aware ownership and access checks. An unpredictable object-storage path is not a substitute for authorization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search and reporting:&lt;/strong&gt; Search indexes, analytics queries, dashboards, and exports must preserve tenant boundaries. A correctly scoped API does not help if a reporting query aggregates records across organizations unintentionally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs and audit trails:&lt;/strong&gt; Tenant identifiers should be included where useful for traceability, while sensitive information should be handled according to appropriate logging and retention policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration:&lt;/strong&gt; Tenant-specific settings should be resolved within the active organizational context. One tenant's feature flags, branding, or business rules should not inadvertently affect another.&lt;/p&gt;

&lt;p&gt;These concerns become especially important as the application grows. A new integration or background-processing feature can introduce a data-access path that bypasses assumptions enforced elsewhere.&lt;/p&gt;

&lt;p&gt;Tenant isolation needs to be considered across the complete system, not just its primary database tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing tenant boundaries
&lt;/h2&gt;

&lt;p&gt;Multi-tenancy also changes what needs to be tested.&lt;/p&gt;

&lt;p&gt;Testing whether a user can retrieve their own records is not enough. We must also test whether that user is prevented from accessing another tenant's records.&lt;/p&gt;

&lt;p&gt;Useful scenarios include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a user cannot retrieve a record belonging to another tenant by changing its identifier;&lt;/li&gt;
&lt;li&gt;a user cannot update or delete another tenant's resource;&lt;/li&gt;
&lt;li&gt;a tenant administrator cannot administer a different tenant;&lt;/li&gt;
&lt;li&gt;a user with different roles in different organizations receives the correct permissions in each context;&lt;/li&gt;
&lt;li&gt;cache entries and background jobs preserve the correct tenant scope;&lt;/li&gt;
&lt;li&gt;invalid or missing tenant context is rejected where required;&lt;/li&gt;
&lt;li&gt;database constraints prevent cross-tenant relationships;&lt;/li&gt;
&lt;li&gt;tenant-specific exports and reports contain only authorized records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests should exercise the actual authorization and data-access paths, including integration tests against the database where appropriate.&lt;/p&gt;

&lt;p&gt;Where PostgreSQL RLS is part of the security design, its policies should be tested using the same database roles and transaction patterns used by the application. Tests run only as a privileged database owner may fail to reveal problems that affect the real application role.&lt;/p&gt;

&lt;p&gt;The goal is to verify the boundary, not merely the successful execution of ordinary operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this connects to my other work
&lt;/h2&gt;

&lt;p&gt;The same architectural principles appear in enterprise systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identity;&lt;/li&gt;
&lt;li&gt;organization;&lt;/li&gt;
&lt;li&gt;authorization;&lt;/li&gt;
&lt;li&gt;data boundaries;&lt;/li&gt;
&lt;li&gt;auditability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The domain may change from schools to government offices, but the architectural questions are surprisingly similar.&lt;/p&gt;

&lt;p&gt;In both cases, an identity operates within an organizational structure, and access depends on the relationship between that identity, the organization, and the requested resource.&lt;/p&gt;

&lt;p&gt;A school-management platform may organize data around schools, classes, students, and teachers. An enterprise or government system may organize data around departments, offices, personnel, assets, and approvals.&lt;/p&gt;

&lt;p&gt;The domain models differ, but both systems need to establish ownership, enforce contextual permissions, preserve traceability, and prevent one organizational unit from accessing another's data without authorization.&lt;/p&gt;

&lt;p&gt;This is why I see multi-tenancy as more than a SaaS feature. It is an exercise in making organizational boundaries explicit and enforceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Multi-tenancy is not simply adding a &lt;code&gt;tenantId&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;It is a system-wide data and authorization boundary.&lt;/p&gt;

&lt;p&gt;That boundary must be reflected consistently across authentication, membership, authorization, database access, caching, background jobs, file storage, reporting, and testing.&lt;/p&gt;

&lt;p&gt;A shared application can serve many organizations effectively, but only when the system makes it difficult for one tenant's operations to cross into another tenant's data unintentionally.&lt;/p&gt;

&lt;p&gt;The objective is not merely to support multiple organizations.&lt;/p&gt;

&lt;p&gt;It is to support them without compromising the isolation, integrity, and accountability each organization expects.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>node</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Building Frontends That Respect Backend Architecture</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:53:00 +0000</pubDate>
      <link>https://dev.to/topzyray/building-frontends-that-respect-backend-architecture-1g7p</link>
      <guid>https://dev.to/topzyray/building-frontends-that-respect-backend-architecture-1g7p</guid>
      <description>&lt;p&gt;&lt;em&gt;"Apply clear frontend boundaries by separating server and client state, centralizing API access, and organizing code around features."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Although much of my recent architectural focus has been backend engineering, full-stack systems have reminded me that frontend architecture also needs boundaries.&lt;/p&gt;

&lt;p&gt;A backend can be beautifully designed and still become difficult to work with if the frontend mixes networking, server state, local state and UI behavior indiscriminately.&lt;/p&gt;

&lt;p&gt;When every component is responsible for fetching data, handling errors, managing loading indicators, transforming responses and maintaining shared state, the application gradually becomes harder to reason about.&lt;/p&gt;

&lt;p&gt;Small changes begin to affect unrelated components. API changes spread across the UI. State becomes difficult to trace. Components become tightly coupled to implementation details that should have remained behind a boundary.&lt;/p&gt;

&lt;p&gt;These are not necessarily problems with React or any particular frontend framework.&lt;/p&gt;

&lt;p&gt;They are architectural problems.&lt;/p&gt;

&lt;p&gt;The same principles that make backend systems easier to maintain—separation of concerns, explicit contracts and clear ownership—also apply to frontend engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate server state from client state
&lt;/h2&gt;

&lt;p&gt;This distinction is particularly important.&lt;/p&gt;

&lt;p&gt;Data retrieved from an API is server state.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users;&lt;/li&gt;
&lt;li&gt;requisitions;&lt;/li&gt;
&lt;li&gt;inventory;&lt;/li&gt;
&lt;li&gt;notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;UI state is different.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;modal visibility;&lt;/li&gt;
&lt;li&gt;selected filters;&lt;/li&gt;
&lt;li&gt;temporary form state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two categories should not automatically be managed the same way.&lt;/p&gt;

&lt;p&gt;Server state represents information owned by an external system, usually the backend. It may become stale, change independently of the current browser session, require refetching, and need synchronization after mutations.&lt;/p&gt;

&lt;p&gt;Client state represents information that the frontend owns for the current interaction or application experience.&lt;/p&gt;

&lt;p&gt;For example, a requisition list may be server state, while the currently selected status filter is client state. The filtered result may be derived from those two values rather than stored as another independent copy.&lt;/p&gt;

&lt;p&gt;This distinction matters because every additional copy of the same data creates another opportunity for inconsistency.&lt;/p&gt;

&lt;p&gt;If a requisition is approved, the frontend should not have to manually update several unrelated stores and component states to keep the interface synchronized.&lt;/p&gt;

&lt;p&gt;Instead, the architecture should make it clear where the authoritative data comes from and how the UI responds when that data changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Query
&lt;/h2&gt;

&lt;p&gt;For server state, TanStack Query (formerly React Query) provides useful mechanisms around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetching;&lt;/li&gt;
&lt;li&gt;caching;&lt;/li&gt;
&lt;li&gt;invalidation;&lt;/li&gt;
&lt;li&gt;mutations;&lt;/li&gt;
&lt;li&gt;loading states;&lt;/li&gt;
&lt;li&gt;error states.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That reduces the temptation to put every API response into a global client store.&lt;/p&gt;

&lt;p&gt;It also provides a consistent approach to concerns that otherwise tend to be implemented repeatedly across components.&lt;/p&gt;

&lt;p&gt;For example, a requisition query can be defined independently of the component that displays the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tanstack/react-query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requisitionsApi&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/api/requisitions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useRequisitions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;requisitions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;requisitionsApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A page or component can consume the query and focus on presentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRequisitions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/hooks/use-requisitions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RequisitionList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requisitions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRequisitions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading requisitions...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Unable to load requisitions.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;requisitions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;requisition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;requisition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;requisition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reference&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These examples assume that the API client returns data in the shape expected by the component. In a production application, that contract should be defined and validated appropriately.&lt;/p&gt;

&lt;p&gt;Mutations should also have an explicit strategy for keeping cached data consistent. Depending on the operation, that might involve invalidating a query, updating cached data, or using an optimistic update with appropriate rollback behavior.&lt;/p&gt;

&lt;p&gt;There is an important distinction here: caching does not make the frontend the source of truth. It provides a mechanism for reusing and synchronizing data that is ultimately owned by the backend.&lt;/p&gt;

&lt;p&gt;Query keys, invalidation rules and mutation behavior are therefore part of the frontend's data architecture, not just implementation details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zustand
&lt;/h2&gt;

&lt;p&gt;A lightweight client-state library such as Zustand can instead handle application state that genuinely belongs on the client.&lt;/p&gt;

&lt;p&gt;For example, an application might need to retain sidebar visibility, a temporary multi-step workflow, or a shared selection used across several components.&lt;/p&gt;

&lt;p&gt;Zustand can provide a simple way to manage that state without introducing unnecessary complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WorkspaceState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sidebarOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;toggleSidebar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useWorkspaceStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WorkspaceState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sidebarOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="na"&gt;toggleSidebar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;sidebarOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sidebarOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The store owns the sidebar's UI state. It does not need to own the API responses displayed inside the sidebar.&lt;/p&gt;

&lt;p&gt;This is the architectural distinction I want to preserve.&lt;/p&gt;

&lt;p&gt;A global store should not become the default destination for every piece of data simply because multiple components need access to it.&lt;/p&gt;

&lt;p&gt;Before adding state to a shared store, it is worth asking whether that state actually needs to be shared, whether it can be derived, and who owns its lifecycle.&lt;/p&gt;

&lt;p&gt;Not every state value needs a library. Local component state remains appropriate for interactions that do not need to be shared, while form libraries can handle more involved form state and validation.&lt;/p&gt;

&lt;p&gt;The architectural distinction is more important than the particular library.&lt;/p&gt;

&lt;h2&gt;
  
  
  API clients
&lt;/h2&gt;

&lt;p&gt;I also prefer keeping HTTP concerns behind a consistent client boundary.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI
 │
 ▼
Query / Mutation
 │
 ▼
API Client
 │
 ▼
Backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI should not need to understand every Axios configuration detail.&lt;/p&gt;

&lt;p&gt;A centralized API client can own concerns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;base URLs and request configuration;&lt;/li&gt;
&lt;li&gt;authentication and credential handling;&lt;/li&gt;
&lt;li&gt;request and response interceptors;&lt;/li&gt;
&lt;li&gt;timeout behavior;&lt;/li&gt;
&lt;li&gt;consistent error normalization;&lt;/li&gt;
&lt;li&gt;request metadata and correlation identifiers, where appropriate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feature-specific API modules can then expose operations in terms that make sense to the application.&lt;/p&gt;

&lt;p&gt;For example, a requisition module might expose &lt;code&gt;list()&lt;/code&gt;, &lt;code&gt;getById()&lt;/code&gt; and &lt;code&gt;approve()&lt;/code&gt;, rather than requiring every component to construct its own HTTP requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;apiClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/api/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Requisition&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/types/requisition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requisitionsApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Requisition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Requisition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/requisitions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Requisition&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Requisition&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`/requisitions/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/approve`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example illustrates the boundary; a real implementation should use the actual response envelope and error contract defined by the backend.&lt;/p&gt;

&lt;p&gt;The benefit is not simply fewer lines of code.&lt;/p&gt;

&lt;p&gt;It is the ability to change HTTP configuration, error handling or transport details without rewriting every component that consumes the API.&lt;/p&gt;

&lt;p&gt;It also gives the frontend a natural place to translate technical transport concerns into application-level behavior.&lt;/p&gt;

&lt;p&gt;However, a shared client should not become an excuse to hide important semantics. Authentication failures, authorization failures, validation errors and transient network failures may require different responses. A generic interceptor should not silently treat them all as equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respect the backend contract
&lt;/h2&gt;

&lt;p&gt;A frontend and backend are not independent systems that happen to communicate over HTTP.&lt;/p&gt;

&lt;p&gt;They are two parts of a system with an explicit contract.&lt;/p&gt;

&lt;p&gt;That contract includes more than endpoint URLs. It also includes request and response shapes, validation rules, authorization behavior, pagination, error formats, and the meaning of successful operations.&lt;/p&gt;

&lt;p&gt;For example, hiding an approval button in the UI does not enforce authorization. The backend must still verify that the current identity has permission to approve the requisition.&lt;/p&gt;

&lt;p&gt;Similarly, frontend validation can improve usability, but the backend remains responsible for enforcing business rules.&lt;/p&gt;

&lt;p&gt;TypeScript helps maintain consistency within the frontend codebase, but a TypeScript interface does not automatically validate data received over the network at runtime.&lt;/p&gt;

&lt;p&gt;Where necessary, runtime schema validation and shared contract definitions can reduce the risk of frontend and backend assumptions drifting apart.&lt;/p&gt;

&lt;p&gt;The objective is not to duplicate the entire backend domain model in the browser. It is to establish a clear, dependable contract between the two sides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next.js
&lt;/h2&gt;

&lt;p&gt;Next.js provides another layer of architectural decisions.&lt;/p&gt;

&lt;p&gt;With the App Router, developers need to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;server components;&lt;/li&gt;
&lt;li&gt;client components;&lt;/li&gt;
&lt;li&gt;routing;&lt;/li&gt;
&lt;li&gt;data fetching;&lt;/li&gt;
&lt;li&gt;loading and error boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These concepts influence where code executes, how data reaches the UI, and which responsibilities belong on the server or in the browser.&lt;/p&gt;

&lt;p&gt;Server Components are useful for rendering content that does not require browser-side interactivity. They can fetch data on the server and keep server-only implementation details out of the client bundle.&lt;/p&gt;

&lt;p&gt;Client Components are appropriate when a feature requires state, event handlers, effects, or browser APIs.&lt;/p&gt;

&lt;p&gt;The distinction should be intentional.&lt;/p&gt;

&lt;p&gt;Marking a large component tree as client-rendered simply because one small section needs interactivity can expand the client-side JavaScript unnecessarily. Conversely, forcing interactive behavior into Server Components creates avoidable friction.&lt;/p&gt;

&lt;p&gt;Data fetching also needs deliberate ownership. Next.js server-side data access and a client-side query cache solve related but different problems.&lt;/p&gt;

&lt;p&gt;Server Components can be appropriate for initial rendering and server-side access to data. TanStack Query is useful when the client needs ongoing query caching, refetching, mutation handling, or synchronization during an interactive session.&lt;/p&gt;

&lt;p&gt;Using both is possible, but their responsibilities should be clear. Otherwise, the same data may be fetched and cached independently without a coherent invalidation strategy.&lt;/p&gt;

&lt;p&gt;Loading and error boundaries deserve similar attention. They should communicate the state of the application meaningfully rather than serve as generic fallbacks that conceal the source of a failure.&lt;/p&gt;

&lt;p&gt;Again, the framework should support the application architecture.&lt;/p&gt;

&lt;p&gt;It should not determine the architecture by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organize around features and responsibilities
&lt;/h2&gt;

&lt;p&gt;As frontend applications grow, folder structure can either reinforce or weaken these boundaries.&lt;/p&gt;

&lt;p&gt;A feature-oriented structure can keep related API operations, query hooks, types and components close to one another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── app/
│   ├── requisitions/
│   └── inventory/
├── components/
│   ├── ui/
│   └── layout/
├── features/
│   ├── requisitions/
│   │   ├── api/
│   │   ├── hooks/
│   │   ├── components/
│   │   ├── schemas/
│   │   └── types/
│   └── inventory/
│       ├── api/
│       ├── hooks/
│       ├── components/
│       ├── schemas/
│       └── types/
├── lib/
│   └── api/
└── stores/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one possible arrangement, not a universal prescription.&lt;/p&gt;

&lt;p&gt;A small application may not need all these folders. A larger application may benefit from stronger feature boundaries and explicit shared contracts.&lt;/p&gt;

&lt;p&gt;The underlying principle is that related responsibilities should be easy to locate, while unrelated features should not become tightly coupled through shared components or global state.&lt;/p&gt;

&lt;p&gt;A folder structure alone cannot guarantee good architecture. The imports, ownership rules and dependencies between modules matter more than the names of the directories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Full-stack engineering is not about treating frontend and backend as unrelated applications.&lt;/p&gt;

&lt;p&gt;The contracts between them are part of the architecture.&lt;/p&gt;

&lt;p&gt;Server state and client state have different responsibilities. API clients should isolate transport concerns. Components should focus on presenting and interacting with data. Framework features should be used according to their strengths.&lt;/p&gt;

&lt;p&gt;These boundaries reduce unnecessary coupling and make changes easier to reason about.&lt;/p&gt;

&lt;p&gt;A clean API boundary makes both sides easier to evolve.&lt;/p&gt;

&lt;p&gt;A clean frontend architecture helps ensure that the backend's design remains an asset rather than becoming obscured by the way the UI consumes it.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>backenddevelopment</category>
      <category>react</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Performance Engineering: Measure Before You Optimize</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Mon, 26 Oct 2026 18:03:42 +0000</pubDate>
      <link>https://dev.to/topzyray/performance-engineering-measure-before-you-optimize-5ebm</link>
      <guid>https://dev.to/topzyray/performance-engineering-measure-before-you-optimize-5ebm</guid>
      <description>&lt;p&gt;&lt;em&gt;"Improve backend performance through representative benchmarks, meaningful baselines, and measurements that identify real system bottlenecks."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Performance engineering is often presented as a collection of optimization tricks.&lt;/p&gt;

&lt;p&gt;Use caching.&lt;/p&gt;

&lt;p&gt;Use indexes.&lt;/p&gt;

&lt;p&gt;Use queues.&lt;/p&gt;

&lt;p&gt;Use clustering.&lt;/p&gt;

&lt;p&gt;Use streams.&lt;/p&gt;

&lt;p&gt;These can all be useful.&lt;/p&gt;

&lt;p&gt;But before choosing an optimization, measure the behavior you want to improve.&lt;/p&gt;

&lt;p&gt;Without measurement, we risk solving the wrong problem, introducing unnecessary complexity, or optimizing a component that contributes very little to the overall performance of the system.&lt;/p&gt;

&lt;p&gt;A technique that improves one workload may have little effect on another. Some optimizations also introduce trade-offs involving memory, consistency, operational complexity, and failure handling.&lt;/p&gt;

&lt;p&gt;The goal is not to make every component faster in isolation.&lt;/p&gt;

&lt;p&gt;The goal is to understand how the system behaves under a defined workload and improve the parts that matter most.&lt;/p&gt;

&lt;h2&gt;
  
  
  An assumption is not a benchmark
&lt;/h2&gt;

&lt;p&gt;Suppose we believe an implementation is faster.&lt;/p&gt;

&lt;p&gt;That is an assumption.&lt;/p&gt;

&lt;p&gt;A benchmark gives us evidence.&lt;/p&gt;

&lt;p&gt;Here, a benchmark means a repeatable measurement of a defined workload, not necessarily a full load, stress, or soak test.&lt;/p&gt;

&lt;p&gt;A useful benchmark should define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what is being measured;&lt;/li&gt;
&lt;li&gt;the workload;&lt;/li&gt;
&lt;li&gt;the environment;&lt;/li&gt;
&lt;li&gt;the baseline;&lt;/li&gt;
&lt;li&gt;the change;&lt;/li&gt;
&lt;li&gt;the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should also account for factors that can influence the result, such as warm-up time, concurrency, cache state, database state, network conditions, and background activity.&lt;/p&gt;

&lt;p&gt;For example, comparing two API implementations using a handful of sequential requests may produce a different result from testing them under sustained concurrent traffic.&lt;/p&gt;

&lt;p&gt;The first test might suggest that both implementations are fast. The second may reveal connection-pool exhaustion, increasing response times, or a growing queue of pending work.&lt;/p&gt;

&lt;p&gt;The benchmark must reflect the question we are trying to answer.&lt;/p&gt;

&lt;p&gt;If we want to know whether an implementation improves throughput, we should measure throughput under comparable conditions. If we want to know whether it improves the experience of individual users, we should pay close attention to latency.&lt;/p&gt;

&lt;p&gt;A benchmark without a reliable baseline is difficult to interpret. A result without a repeatable process is difficult to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the right thing
&lt;/h2&gt;

&lt;p&gt;For backend systems, performance is not just requests per second.&lt;/p&gt;

&lt;p&gt;Useful measurements can include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; How long does a request or operation take?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput:&lt;/strong&gt; How much work does the system complete within a given period?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error rate:&lt;/strong&gt; How often do requests fail, time out, or return unexpected responses?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory usage:&lt;/strong&gt; How much memory does the process consume, and does that usage continue to grow?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU usage:&lt;/strong&gt; Is the application spending too much time performing computation?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database time:&lt;/strong&gt; How much time is spent executing queries and waiting for database operations?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue delay:&lt;/strong&gt; How long does work wait before a worker begins processing it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource saturation:&lt;/strong&gt; Are CPU, memory, database connections, file descriptors, or other resources approaching their limits?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These measurements answer different questions.&lt;/p&gt;

&lt;p&gt;A system can have high throughput while individual requests experience unacceptable delays. It can also have low average latency while a small but important percentage of requests take several seconds to complete.&lt;/p&gt;

&lt;p&gt;That is why latency percentiles matter: they can expose slow requests that averages conceal, as described in the &lt;a href="https://sre.google/workbook/monitoring/" rel="noopener noreferrer"&gt;Google SRE Workbook’s monitoring guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Consider these common metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;p50:&lt;/strong&gt; The median request latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p95:&lt;/strong&gt; The latency at or below which approximately 95% of requests in the measured sample complete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p99:&lt;/strong&gt; The latency at or below which approximately 99% of requests in the measured sample complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The p95 and p99 measurements can reveal slow requests that an average may conceal. They are particularly useful when a system has variable workloads, external dependencies, or occasional resource contention.&lt;/p&gt;

&lt;p&gt;Error rate matters just as much. An implementation that processes more requests per second by rejecting or timing out more requests is not necessarily an improvement.&lt;/p&gt;

&lt;p&gt;Performance measurements should therefore be interpreted together rather than in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Establish a baseline first
&lt;/h2&gt;

&lt;p&gt;Before changing an implementation, we need to understand its current behavior.&lt;/p&gt;

&lt;p&gt;A baseline provides a reference point for evaluating whether an optimization actually helps.&lt;/p&gt;

&lt;p&gt;For an API endpoint, that baseline might include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request latency at p50, p95, and p99;&lt;/li&gt;
&lt;li&gt;throughput at a defined concurrency level;&lt;/li&gt;
&lt;li&gt;error and timeout rates;&lt;/li&gt;
&lt;li&gt;CPU and memory consumption;&lt;/li&gt;
&lt;li&gt;database query duration;&lt;/li&gt;
&lt;li&gt;database connection-pool utilization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact measurements depend on the system and the performance question.&lt;/p&gt;

&lt;p&gt;It is also important to record the conditions under which the measurements were collected. Runtime versions, hardware, container limits, database configuration, dataset size, and request patterns can all influence the outcome.&lt;/p&gt;

&lt;p&gt;For example, an endpoint tested against a database containing a few hundred records may behave very differently when the production database contains millions.&lt;/p&gt;

&lt;p&gt;A baseline does not need to be complicated. It needs to be representative, repeatable, and sufficiently detailed to support a meaningful comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the actual workload
&lt;/h2&gt;

&lt;p&gt;A benchmark is only useful when its workload resembles the behavior we care about.&lt;/p&gt;

&lt;p&gt;Consider an API that normally receives requests from many concurrent users. Testing it with one request at a time may hide contention and resource limitations.&lt;/p&gt;

&lt;p&gt;A more representative test might account for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the number of concurrent users or requests;&lt;/li&gt;
&lt;li&gt;the proportion of reads to writes;&lt;/li&gt;
&lt;li&gt;typical and unusually large payloads;&lt;/li&gt;
&lt;li&gt;the size and distribution of database records;&lt;/li&gt;
&lt;li&gt;cache hit and miss rates;&lt;/li&gt;
&lt;li&gt;external service dependencies;&lt;/li&gt;
&lt;li&gt;bursts of traffic and sustained load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is also useful to distinguish between different types of performance tests.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;load test&lt;/strong&gt; evaluates behavior under an expected workload. A &lt;strong&gt;stress test&lt;/strong&gt; pushes the system beyond its expected operating conditions to identify its limits and failure behavior. A &lt;strong&gt;soak test&lt;/strong&gt; sustains a workload over a longer period to expose issues such as memory growth, resource leaks, or gradual degradation.&lt;/p&gt;

&lt;p&gt;These tests answer different questions. A single benchmark rarely answers all of them.&lt;/p&gt;

&lt;p&gt;We should also avoid treating a short-lived peak result as proof of sustainable performance. A system may perform well for a brief period before its connection pools, memory limits, or downstream dependencies become saturated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database performance
&lt;/h2&gt;

&lt;p&gt;A slow API may not actually have a slow Node.js layer.&lt;/p&gt;

&lt;p&gt;The database query may be the bottleneck.&lt;/p&gt;

&lt;p&gt;That is why performance investigation needs to cross architectural boundaries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP
 │
 ▼
Application
 │
 ▼
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every layer can contribute to latency.&lt;/p&gt;

&lt;p&gt;The total response time may include network overhead, request parsing, application logic, database connection acquisition, query execution, result processing, serialization, and response transmission.&lt;/p&gt;

&lt;p&gt;If we focus only on the application code, we may miss the operation consuming most of the time.&lt;/p&gt;

&lt;p&gt;For example, an endpoint that retrieves a list of requisitions might execute a separate query for every requisition to retrieve its items. As the number of requisitions grows, the number of database queries grows with it.&lt;/p&gt;

&lt;p&gt;This is a common form of the &lt;a href="https://www.prisma.io/docs/orm/v7/prisma-client/queries/advanced/query-optimization-performance#solving-the-n1-problem" rel="noopener noreferrer"&gt;N+1 query problem&lt;/a&gt;: one query retrieves the parent records, then an additional query runs for each record to retrieve its related items.&lt;/p&gt;

&lt;p&gt;Adding more Node.js workers will not necessarily solve it. In some cases, it may increase database contention by allowing more concurrent queries to reach an already overloaded database.&lt;/p&gt;

&lt;p&gt;A better investigation would examine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the number of queries executed per request;&lt;/li&gt;
&lt;li&gt;query execution plans;&lt;/li&gt;
&lt;li&gt;indexes and their effectiveness;&lt;/li&gt;
&lt;li&gt;the volume of data retrieved;&lt;/li&gt;
&lt;li&gt;unnecessary database round trips;&lt;/li&gt;
&lt;li&gt;connection-pool utilization;&lt;/li&gt;
&lt;li&gt;transaction duration and lock contention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL's &lt;a href="https://www.postgresql.org/docs/current/sql-explain.html" rel="noopener noreferrer"&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;&lt;/a&gt; executes the statement and reports actual runtime statistics alongside the query plan. Use care with statements that modify data, since their side effects occur.&lt;/p&gt;

&lt;p&gt;The important point is that database performance should be measured at the database boundary, not inferred from API response time alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Node.js performance
&lt;/h2&gt;

&lt;p&gt;When the database is not the bottleneck, we need to investigate the Node.js runtime and application code.&lt;/p&gt;

&lt;p&gt;Node.js is well suited to I/O-heavy workloads, but that does not mean every Node.js application is automatically fast.&lt;/p&gt;

&lt;p&gt;CPU-intensive JavaScript can block the event loop. As the &lt;a href="https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop" rel="noopener noreferrer"&gt;Node.js event-loop guidance&lt;/a&gt; explains, long-running callbacks can reduce throughput and delay other clients. Large synchronous operations can delay unrelated requests. Excessive object creation can increase garbage-collection activity. Unbounded concurrency can overwhelm downstream services.&lt;/p&gt;

&lt;p&gt;The appropriate response depends on the evidence.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If CPU usage is high and the event loop is frequently blocked, investigate expensive computations and synchronous operations.&lt;/li&gt;
&lt;li&gt;If requests spend most of their time waiting for a database, investigate query execution, connection pools, and database capacity.&lt;/li&gt;
&lt;li&gt;If memory usage grows continuously, investigate retained objects, unbounded collections, and resource cleanup.&lt;/li&gt;
&lt;li&gt;If latency increases sharply under concurrency, examine resource contention, connection limits, queueing, and downstream saturation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node.js provides diagnostic capabilities such as CPU profiling and heap snapshots. The &lt;a href="https://nodejs.org/learn/getting-started/profiling" rel="noopener noreferrer"&gt;Node.js diagnostics guide&lt;/a&gt; describes tools that can help narrow down performance problems instead of relying on guesswork.&lt;/p&gt;

&lt;p&gt;Profiling should generally be performed under a workload that reproduces the behavior being investigated. A CPU profile collected while the application is idle will not explain a CPU bottleneck that appears only under heavy traffic.&lt;/p&gt;

&lt;p&gt;The objective is to connect an observed symptom to a specific cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize the bottleneck, not the architecture diagram
&lt;/h2&gt;

&lt;p&gt;Once we identify a bottleneck, we can select an appropriate optimization.&lt;/p&gt;

&lt;p&gt;Caching may reduce repeated database reads. An index may improve query execution. A queue may move non-critical work out of the request-response path. Streams may reduce memory pressure when processing large amounts of data. Worker threads may help with CPU-intensive JavaScript.&lt;/p&gt;

&lt;p&gt;But each technique solves a different problem.&lt;/p&gt;

&lt;p&gt;Caching introduces invalidation and consistency concerns. Indexes consume storage and add overhead to writes, even as they can speed up retrieval (&lt;a href="https://www.postgresql.org/docs/current/indexes.html" rel="noopener noreferrer"&gt;PostgreSQL documentation&lt;/a&gt;). Queues introduce delivery, retry, and idempotency considerations. Streams require correct backpressure handling. Worker threads introduce communication and lifecycle overhead.&lt;/p&gt;

&lt;p&gt;Even clustering is not a universal solution. It can improve the use of multiple CPU cores for suitable workloads, but it does not automatically fix slow queries, inefficient algorithms, or overloaded external dependencies.&lt;/p&gt;

&lt;p&gt;Before choosing an optimization, we should be able to explain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is the observed performance problem?&lt;/li&gt;
&lt;li&gt;Which component or operation causes it?&lt;/li&gt;
&lt;li&gt;Why should the proposed change improve that behavior?&lt;/li&gt;
&lt;li&gt;What trade-offs could the change introduce?&lt;/li&gt;
&lt;li&gt;Which measurements will demonstrate whether it worked?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These questions keep optimization tied to evidence rather than fashion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarking belongs in engineering
&lt;/h2&gt;

&lt;p&gt;As I continue studying advanced Node.js, I want benchmarking to become part of the engineering process rather than something performed only when a system becomes slow.&lt;/p&gt;

&lt;p&gt;The sequence should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Measure
  ↓
Identify bottleneck
  ↓
Form a hypothesis
  ↓
Change
  ↓
Measure again
  ↓
Compare results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comparison should use the same workload and sufficiently similar environmental conditions. Otherwise, we may attribute an improvement or regression to a code change when another variable actually caused it.&lt;/p&gt;

&lt;p&gt;We should also retain the results of important performance investigations. A record of the baseline, hypothesis, change, and outcome helps the team understand why an optimization was introduced and whether it continues to provide value.&lt;/p&gt;

&lt;p&gt;Not every optimization needs a dedicated benchmarking suite. Small, low-risk changes may only require focused measurements. Performance-critical paths, recurring workloads, and systems with strict latency or throughput requirements may justify automated benchmarks and regression checks.&lt;/p&gt;

&lt;p&gt;The level of rigor should reflect the cost of getting the decision wrong.&lt;/p&gt;

&lt;p&gt;Performance engineering is not about running benchmarks for their own sake. It is about making informed decisions and verifying their results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Optimization without measurement is speculation.&lt;/p&gt;

&lt;p&gt;Production performance engineering requires evidence.&lt;/p&gt;

&lt;p&gt;The fastest-looking implementation is not necessarily the fastest under realistic conditions. The most sophisticated architecture is not necessarily the most efficient. And a successful optimization is not one that merely looks cleaner or more advanced.&lt;/p&gt;

&lt;p&gt;It is one that produces a measurable improvement without introducing unacceptable trade-offs.&lt;/p&gt;

&lt;p&gt;Measure first. Understand the bottleneck. Make a targeted change. Measure again.&lt;/p&gt;

</description>
      <category>node</category>
      <category>backenddevelopment</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>Going Deeper Into Node.js: Understanding the Runtime Behind the Framework</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:35:15 +0000</pubDate>
      <link>https://dev.to/topzyray/going-deeper-into-nodejs-understanding-the-runtime-behind-the-framework-4n03</link>
      <guid>https://dev.to/topzyray/going-deeper-into-nodejs-understanding-the-runtime-behind-the-framework-4n03</guid>
      <description>&lt;p&gt;&lt;em&gt;"Go beyond framework APIs to understand the &lt;a href="https://nodejs.org/learn/asynchronous-work/event-loop-timers-and-nexttick" rel="noopener noreferrer"&gt;Node.js event loop&lt;/a&gt;, concurrency, streams, memory, networking, and graceful shutdown."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The more serious my backend engineering work becomes, the less satisfied I am with knowing only the framework APIs.&lt;/p&gt;

&lt;p&gt;NestJS and Express are useful abstractions.&lt;/p&gt;

&lt;p&gt;But eventually, understanding the runtime underneath them becomes important.&lt;/p&gt;

&lt;p&gt;A framework can help organize an application, enforce architectural boundaries, and simplify common backend operations. It cannot eliminate the runtime behavior of the code we write.&lt;/p&gt;

&lt;p&gt;When an application becomes slow, memory consumption increases, requests begin timing out, or background work interferes with API responsiveness, the explanation may have little to do with the framework itself.&lt;/p&gt;

&lt;p&gt;It may be a consequence of &lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; executes JavaScript, manages asynchronous operations, handles data, or uses system resources.&lt;/p&gt;

&lt;p&gt;That is why I consider runtime knowledge an important part of backend engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework is not the runtime
&lt;/h2&gt;

&lt;p&gt;When we write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;there is a substantial amount happening underneath.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; manages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript execution.&lt;/li&gt;
&lt;li&gt;Asynchronous operations.&lt;/li&gt;
&lt;li&gt;Event-loop scheduling.&lt;/li&gt;
&lt;li&gt;Networking.&lt;/li&gt;
&lt;li&gt;Timers.&lt;/li&gt;
&lt;li&gt;Streams.&lt;/li&gt;
&lt;li&gt;Process behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also provides access to operating-system capabilities through its APIs and underlying runtime components.&lt;/p&gt;

&lt;p&gt;Understanding these mechanisms helps explain application behavior.&lt;/p&gt;

&lt;p&gt;For example, NestJS may handle dependency injection, route registration, guards, interceptors, and exception filters. Express may provide middleware composition and HTTP routing.&lt;/p&gt;

&lt;p&gt;But neither framework changes the fundamental execution model of JavaScript on the main thread.&lt;/p&gt;

&lt;p&gt;A poorly designed synchronous operation can still block request processing, regardless of how cleanly the application is organized.&lt;/p&gt;

&lt;p&gt;Similarly, a memory-intensive operation can still exhaust the process's available memory, even when every service follows a well-defined architecture.&lt;/p&gt;

&lt;p&gt;Framework abstractions simplify development. They do not remove the need to understand the system underneath them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the event loop matters
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; is designed around asynchronous I/O.&lt;/p&gt;

&lt;p&gt;That makes it effective for many backend workloads, particularly applications that spend substantial time waiting for databases, network responses, files, and other external resources.&lt;/p&gt;

&lt;p&gt;But asynchronous syntax does not automatically make every operation cheap.&lt;/p&gt;

&lt;p&gt;A CPU-heavy operation can still block JavaScript execution.&lt;/p&gt;

&lt;p&gt;That means backend engineers need to understand the difference between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I/O-bound work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPU-bound work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I/O-bound work spends much of its time waiting for an external operation to complete. CPU-bound work spends much of its time performing computations.&lt;/p&gt;

&lt;p&gt;Consider a typical request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;permissionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getForUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the repository and permission service perform asynchronous I/O, the JavaScript execution can yield while those operations are pending.&lt;/p&gt;

&lt;p&gt;This allows &lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; to make progress on other work rather than blocking the main thread while waiting for each response.&lt;/p&gt;

&lt;p&gt;However, &lt;code&gt;await&lt;/code&gt; does not move the underlying computation to another thread automatically.&lt;/p&gt;

&lt;p&gt;When an awaited operation is CPU-intensive JavaScript, the computation still runs on the thread executing that JavaScript.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expensiveCalculation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;largeDataset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;expensiveCalculation&lt;/code&gt; takes several seconds to execute synchronously, other JavaScript callbacks on that thread must wait.&lt;/p&gt;

&lt;p&gt;This can increase API latency, delay timers, and prevent the application from responding promptly to other requests.&lt;/p&gt;

&lt;p&gt;The important distinction is not whether a function is declared &lt;code&gt;async&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is what the function actually does and where that work executes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the event loop beyond &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The event loop is central to how &lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; coordinates JavaScript execution and asynchronous operations.&lt;/p&gt;

&lt;p&gt;At a high level, the runtime executes JavaScript callbacks and processes scheduled work as the relevant operations become ready.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; uses the event loop and underlying mechanisms such as libuv to coordinate I/O, timers, and other asynchronous activity.&lt;/p&gt;

&lt;p&gt;Promises and &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; also involve microtask scheduling, which affects the order in which callbacks execute.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a normal execution of this example, the output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A
D
C
B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The synchronous statements execute first. The resolved promise's reaction runs as a microtask before the timer callback in this example.&lt;/p&gt;

&lt;p&gt;The exact scheduling behavior becomes more nuanced when multiple event-loop phases, I/O callbacks, and different scheduling APIs are involved.&lt;/p&gt;

&lt;p&gt;The practical lesson is that asynchronous code is not necessarily executed in the order in which developers intuitively expect it to be.&lt;/p&gt;

&lt;p&gt;Understanding scheduling becomes particularly useful when debugging race conditions, unexpected callback ordering, delayed timers, and performance problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asynchronous does not mean parallel
&lt;/h2&gt;

&lt;p&gt;One distinction worth understanding is the difference between concurrency and parallelism.&lt;/p&gt;

&lt;p&gt;Concurrency means multiple operations can make progress over overlapping periods.&lt;/p&gt;

&lt;p&gt;Parallelism means multiple operations execute at the same time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; can handle many concurrent I/O operations without requiring a separate JavaScript thread for each request.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;settingsService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getForUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If these operations are independent, &lt;code&gt;Promise.all&lt;/code&gt; allows them to be initiated together rather than waiting for each one to finish before starting the next.&lt;/p&gt;

&lt;p&gt;However, this does not mean the JavaScript computations inside all three operations execute in parallel on the main thread.&lt;/p&gt;

&lt;p&gt;It also does not mean unlimited concurrency is desirable.&lt;/p&gt;

&lt;p&gt;Launching thousands of database queries simultaneously can exhaust connection pools, overload the database, and increase latency across the application.&lt;/p&gt;

&lt;p&gt;Concurrency must be managed according to the capacity of the dependencies involved.&lt;/p&gt;

&lt;p&gt;For large workloads, bounded concurrency, batching, backpressure, and queue-based processing can be more appropriate than creating an unrestricted number of concurrent operations.&lt;/p&gt;

&lt;p&gt;The goal is not simply to make more operations concurrent.&lt;/p&gt;

&lt;p&gt;It is to make progress efficiently without overwhelming the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU-heavy work needs a different strategy
&lt;/h2&gt;

&lt;p&gt;Some workloads are naturally expensive in terms of CPU.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large data transformations.&lt;/li&gt;
&lt;li&gt;Complex report generation.&lt;/li&gt;
&lt;li&gt;Image processing.&lt;/li&gt;
&lt;li&gt;Compression and decompression.&lt;/li&gt;
&lt;li&gt;Cryptographic computations.&lt;/li&gt;
&lt;li&gt;Computationally intensive parsing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If such work executes synchronously on the main JavaScript thread, it can delay unrelated requests.&lt;/p&gt;

&lt;p&gt;There are several possible responses, depending on the workload.&lt;/p&gt;

&lt;p&gt;First, optimize the algorithm. Reducing unnecessary computation is often more effective than adding infrastructure.&lt;/p&gt;

&lt;p&gt;Second, split work into manageable units where doing so is practical.&lt;/p&gt;

&lt;p&gt;Third, use worker threads when CPU-intensive JavaScript can benefit from execution on another thread.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="https://nodejs.org/api/worker_threads.html" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; provides the &lt;a href="https://nodejs.org/api/worker_threads.html" rel="noopener noreferrer"&gt;&lt;code&gt;worker_threads&lt;/code&gt;&lt;/a&gt; module for running JavaScript in parallel across threads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:worker_threads&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./calculation.worker.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;largeDataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an illustrative example of creating a worker; a production implementation also needs result handling, error handling, worker lifecycle management, and an appropriate strategy for reusing or pooling workers.&lt;/p&gt;

&lt;p&gt;Creating a new worker for every small task can introduce unnecessary overhead.&lt;/p&gt;

&lt;p&gt;For sustained workloads, a worker pool may be more suitable.&lt;/p&gt;

&lt;p&gt;It is also important to distinguish worker threads from child processes. Worker threads execute JavaScript in separate threads within the same process, while child processes run in separate operating-system processes with their own memory spaces and lifecycle.&lt;/p&gt;

&lt;p&gt;Neither approach automatically makes a workload efficient. The algorithm, data-transfer costs, available CPU resources, and operational requirements still matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streams matter too
&lt;/h2&gt;

&lt;p&gt;Large files and large data sets should not always be loaded completely into memory.&lt;/p&gt;

&lt;p&gt;Streams provide a mechanism for processing data incrementally.&lt;/p&gt;

&lt;p&gt;This becomes particularly relevant to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uploads.&lt;/li&gt;
&lt;li&gt;Downloads.&lt;/li&gt;
&lt;li&gt;Exports.&lt;/li&gt;
&lt;li&gt;Imports.&lt;/li&gt;
&lt;li&gt;Large transformations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider an export that retrieves hundreds of thousands of database records.&lt;/p&gt;

&lt;p&gt;One approach is to load every record into an array, transform the entire array, and then generate the output.&lt;/p&gt;

&lt;p&gt;That can consume substantial memory, especially when several exports run concurrently.&lt;/p&gt;

&lt;p&gt;A streaming approach processes data incrementally, allowing the application to work with smaller chunks rather than retaining the entire result in memory.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database Cursor
      │
      ▼
Transform Records
      │
      ▼
CSV Formatter
      │
      ▼
HTTP Response or File
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://nodejs.org/api/stream.html" rel="noopener noreferrer"&gt;Node.js streams&lt;/a&gt;  support this kind of processing.&lt;/p&gt;

&lt;p&gt;However, streams do not automatically make every pipeline memory-safe. The source must also avoid buffering the entire dataset, and the pipeline must handle backpressure correctly.&lt;/p&gt;

&lt;p&gt;Backpressure allows a slower downstream consumer to influence how quickly upstream components produce data.&lt;/p&gt;

&lt;p&gt;Without appropriate backpressure handling, a fast producer can overwhelm a slower consumer and accumulate data in memory.&lt;/p&gt;

&lt;p&gt;For stream pipelines, &lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; provides utilities such as &lt;code&gt;pipeline&lt;/code&gt; from &lt;code&gt;node:stream/promises&lt;/code&gt;, which help coordinate stream completion and error propagation.&lt;/p&gt;

&lt;p&gt;For large imports and exports, I also consider batching, database cursor behavior, cancellation, and what happens when the destination disconnects or fails.&lt;/p&gt;

&lt;p&gt;The objective is not merely to use streams because the files are large.&lt;/p&gt;

&lt;p&gt;It is to design a pipeline whose memory usage and failure behavior remain manageable as the workload grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory management and garbage collection
&lt;/h2&gt;

&lt;p&gt;Memory behavior is another area where runtime knowledge becomes useful.&lt;/p&gt;

&lt;p&gt;JavaScript provides automatic memory management, so developers do not ordinarily allocate and free memory manually.&lt;/p&gt;

&lt;p&gt;However, automatic garbage collection does not mean memory consumption takes care of itself.&lt;/p&gt;

&lt;p&gt;An application can retain references to objects it no longer needs. Large caches, unbounded collections, accumulated event listeners, and retained request data can all contribute to excessive memory usage.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestHistory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;requestHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simplified example continually accumulates data without a retention policy.&lt;/p&gt;

&lt;p&gt;In a long-running process, the collection can grow indefinitely.&lt;/p&gt;

&lt;p&gt;The actual memory problem may not be a traditional leak in the sense of manually allocated memory that cannot be freed. It may be reachable data that the application continues to retain unnecessarily.&lt;/p&gt;

&lt;p&gt;Garbage collection can reclaim objects that are no longer reachable, but it cannot reclaim objects that remain referenced.&lt;/p&gt;

&lt;p&gt;Memory investigations should therefore consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heap growth over time.&lt;/li&gt;
&lt;li&gt;Retained objects and references.&lt;/li&gt;
&lt;li&gt;Large buffers and external memory.&lt;/li&gt;
&lt;li&gt;Cache size and eviction behavior.&lt;/li&gt;
&lt;li&gt;Concurrent requests and temporary allocations.&lt;/li&gt;
&lt;li&gt;Garbage-collection activity and its effect on latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is also useful to distinguish the JavaScript heap from total process memory. Buffers and other native allocations can contribute to memory consumption outside the JavaScript heap.&lt;/p&gt;

&lt;p&gt;This matters when an application approaches its container or operating-system memory limit even though the reported JavaScript heap does not appear unusually large.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking and connection management
&lt;/h2&gt;

&lt;p&gt;Backend applications spend a substantial amount of time communicating with databases, caches, message brokers, and external APIs.&lt;/p&gt;

&lt;p&gt;Understanding the runtime's networking behavior helps explain why connection management matters.&lt;/p&gt;

&lt;p&gt;Consider a service that creates a new database connection for every request.&lt;/p&gt;

&lt;p&gt;Even if each individual query is fast, the application may incur unnecessary connection setup overhead and overwhelm the database under concurrent load.&lt;/p&gt;

&lt;p&gt;Connection pools help reuse connections and place limits on concurrency.&lt;/p&gt;

&lt;p&gt;The same principle applies to Redis clients and HTTP clients, subject to each client's connection-management model.&lt;/p&gt;

&lt;p&gt;Connection reuse alone, however, does not solve every performance problem.&lt;/p&gt;

&lt;p&gt;A pool that is too small may create queues and increase latency. A pool that is too large may overload the dependency. Requests that wait indefinitely for a connection can consume resources without making progress.&lt;/p&gt;

&lt;p&gt;Timeouts, cancellation, retry policies, and connection limits should reflect the behavior and capacity of the dependency.&lt;/p&gt;

&lt;p&gt;The application should also distinguish between connection failures, timeouts, and application-level errors because they may require different recovery strategies.&lt;/p&gt;

&lt;p&gt;Understanding networking helps developers reason about the complete request path rather than treating every delay as a slow function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Process behavior and graceful shutdown
&lt;/h2&gt;

&lt;p&gt;A Node.js application is also an operating-system process.&lt;/p&gt;

&lt;p&gt;That process can receive termination signals, encounter uncaught failures, run out of memory, or be restarted by its process manager or container platform.&lt;/p&gt;

&lt;p&gt;These events matter for HTTP servers and background workers alike.&lt;/p&gt;

&lt;p&gt;A graceful shutdown strategy should allow the application to stop accepting new work, finish or safely interrupt in-flight operations where possible, close connections, and release resources.&lt;/p&gt;

&lt;p&gt;For a background worker, this may also involve stopping the acquisition of new jobs and allowing active jobs to finish within a defined shutdown period.&lt;/p&gt;

&lt;p&gt;The implementation depends on the framework, queue library, deployment platform, and required shutdown guarantees.&lt;/p&gt;

&lt;p&gt;A process that exits cleanly during development may still lose in-flight work if production shutdown behavior has not been considered.&lt;/p&gt;

&lt;p&gt;This is another reason runtime knowledge extends beyond JavaScript syntax.&lt;/p&gt;

&lt;p&gt;It includes understanding how the application behaves throughout its lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework knowledge is not enough
&lt;/h2&gt;

&lt;p&gt;A senior Node.js engineer should understand what happens beneath the framework abstraction.&lt;/p&gt;

&lt;p&gt;That is why I have been expanding my learning beyond controllers and services into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event-loop behavior.&lt;/li&gt;
&lt;li&gt;Asynchronous execution.&lt;/li&gt;
&lt;li&gt;Streams.&lt;/li&gt;
&lt;li&gt;Networking.&lt;/li&gt;
&lt;li&gt;Memory.&lt;/li&gt;
&lt;li&gt;Worker threads.&lt;/li&gt;
&lt;li&gt;Process behavior.&lt;/li&gt;
&lt;li&gt;Performance measurement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These topics are connected.&lt;/p&gt;

&lt;p&gt;A slow endpoint may be waiting on a database, competing for a connection, performing synchronous CPU-heavy work, or buffering too much data.&lt;/p&gt;

&lt;p&gt;A memory problem may originate in a cache, a stream pipeline, or the accumulation of objects retained by a long-running process.&lt;/p&gt;

&lt;p&gt;A worker may be processing jobs correctly but still reduce API responsiveness if both workloads compete for the same CPU and memory resources.&lt;/p&gt;

&lt;p&gt;Understanding the runtime helps narrow these possibilities before introducing unnecessary architectural complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure before optimizing
&lt;/h2&gt;

&lt;p&gt;Runtime knowledge becomes most useful when combined with measurement.&lt;/p&gt;

&lt;p&gt;Performance problems should be investigated using evidence rather than assumptions.&lt;/p&gt;

&lt;p&gt;Useful measurements include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request latency, including percentiles such as p95 and p99.&lt;/li&gt;
&lt;li&gt;Throughput and error rates.&lt;/li&gt;
&lt;li&gt;Event-loop delay and utilization.&lt;/li&gt;
&lt;li&gt;CPU and memory consumption.&lt;/li&gt;
&lt;li&gt;Garbage-collection behavior.&lt;/li&gt;
&lt;li&gt;Database connection-pool saturation.&lt;/li&gt;
&lt;li&gt;External dependency latency.&lt;/li&gt;
&lt;li&gt;Stream throughput and buffering.&lt;/li&gt;
&lt;li&gt;Background-job processing time and queue depth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; provides diagnostic and profiling capabilities, including performance hooks, CPU profiling, heap snapshots, and event-loop monitoring.&lt;/p&gt;

&lt;p&gt;The right tool depends on the symptom being investigated.&lt;/p&gt;

&lt;p&gt;For example, high event-loop delay may suggest that the JavaScript thread is spending too much time executing callbacks or performing synchronous work. It does not, by itself, identify the exact cause.&lt;/p&gt;

&lt;p&gt;CPU profiles, heap snapshots, traces, and application metrics help establish what is actually happening.&lt;/p&gt;

&lt;p&gt;The same principle applies to optimization.&lt;/p&gt;

&lt;p&gt;An architectural change should address an observed bottleneck or a clearly defined reliability requirement.&lt;/p&gt;

&lt;p&gt;Adding worker threads, Redis, additional processes, or more complex streaming infrastructure without understanding the problem can increase operational complexity without improving the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Framework knowledge helps us build applications.&lt;/p&gt;

&lt;p&gt;Runtime knowledge helps us understand why those applications behave the way they do.&lt;/p&gt;

&lt;p&gt;That distinction becomes increasingly important as systems become more demanding.&lt;/p&gt;

&lt;p&gt;As backend engineers, we should understand not only how to structure services and expose APIs, but also how the runtime executes work, manages resources, coordinates asynchronous operations, and responds to failure.&lt;/p&gt;

&lt;p&gt;The framework is an important part of the application.&lt;/p&gt;

&lt;p&gt;It is not a substitute for understanding the platform on which the application runs.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>backenddevelopment</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Testing Backend Architecture, Not Just Functions</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Fri, 23 Oct 2026 09:27:18 +0000</pubDate>
      <link>https://dev.to/topzyray/testing-backend-architecture-not-just-functions-1hdm</link>
      <guid>https://dev.to/topzyray/testing-backend-architecture-not-just-functions-1hdm</guid>
      <description>&lt;p&gt;&lt;em&gt;"Test backend boundaries, integration behavior, failure handling, and critical workflows to build confidence beyond unit-test coverage."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A backend can have excellent unit-test coverage and still be difficult to trust.&lt;/p&gt;

&lt;p&gt;The reason is that functions are only one part of a system.&lt;/p&gt;

&lt;p&gt;We also need to test boundaries.&lt;/p&gt;

&lt;p&gt;A function may behave correctly in isolation while the repository it calls generates an incorrect query, the message publisher silently loses an event, or the HTTP layer returns an inappropriate response when an operation fails.&lt;/p&gt;

&lt;p&gt;These are not necessarily unit-test failures. They are failures in the relationships between components.&lt;/p&gt;

&lt;p&gt;When designing backend systems, I consider testing part of the architecture itself. The way application logic depends on infrastructure, the way data moves between layers, and the way failures propagate all influence how a system should be tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different tests answer different questions
&lt;/h2&gt;

&lt;p&gt;A unit test can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this business rule behave correctly?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An integration test can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this repository correctly communicate with PostgreSQL?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An API test can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this endpoint correctly connect HTTP input to application behavior?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are different questions.&lt;/p&gt;

&lt;p&gt;Each test type provides a different kind of confidence.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test type&lt;/th&gt;
&lt;th&gt;Primary question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit tests&lt;/td&gt;
&lt;td&gt;Does the isolated business logic behave correctly?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration tests&lt;/td&gt;
&lt;td&gt;Do collaborating components work together correctly?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API tests&lt;/td&gt;
&lt;td&gt;Does the HTTP contract behave as expected?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract tests&lt;/td&gt;
&lt;td&gt;Do independently developed components agree on their interfaces?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End-to-end tests&lt;/td&gt;
&lt;td&gt;Does a complete critical workflow work across the system?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These categories can overlap. An API test, for example, can be an integration test if it exercises the HTTP layer and application dependencies together.&lt;/p&gt;

&lt;p&gt;The important distinction is not the label attached to a test.&lt;/p&gt;

&lt;p&gt;It is the behavior being verified and the dependencies involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test business logic independently
&lt;/h2&gt;

&lt;p&gt;This is one reason I prefer application code to depend on interfaces.&lt;/p&gt;

&lt;p&gt;A use case can be tested with fake implementations.&lt;/p&gt;

&lt;p&gt;That means a test can focus on the business rule without requiring every infrastructure dependency.&lt;/p&gt;

&lt;p&gt;Consider an application service that approves a requisition.&lt;/p&gt;

&lt;p&gt;Its responsibilities might include checking whether the requisition is eligible for approval, verifying the actor's permissions through an appropriate application boundary, updating the requisition, and requesting any required follow-up work.&lt;/p&gt;

&lt;p&gt;The business rule should not need a live Redis instance, an Azure Service Bus connection, or a running email provider merely to determine whether an approval is valid.&lt;/p&gt;

&lt;p&gt;A simplified example might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;RequisitionRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Requisition&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requisition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Requisition&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ApprovalPublisher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequisitionApproved&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An application use case can depend on these interfaces rather than concrete infrastructure clients.&lt;/p&gt;

&lt;p&gt;A unit test can then provide controlled implementations of the dependencies and verify the important behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An eligible requisition can be approved.&lt;/li&gt;
&lt;li&gt;An ineligible requisition is rejected.&lt;/li&gt;
&lt;li&gt;An unauthorized actor cannot approve it.&lt;/li&gt;
&lt;li&gt;The correct business state is produced.&lt;/li&gt;
&lt;li&gt;The appropriate event or follow-up action is requested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test remains focused on the application's rules.&lt;/p&gt;

&lt;p&gt;However, a fake repository cannot prove that the real repository correctly implements persistence. That is a separate responsibility requiring integration tests.&lt;/p&gt;

&lt;p&gt;Abstraction makes isolated testing easier, but it does not remove the need to verify the real implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration tests still matter
&lt;/h2&gt;

&lt;p&gt;Abstraction does not eliminate integration testing.&lt;/p&gt;

&lt;p&gt;If PostgreSQL is part of production, we need confidence that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queries work.&lt;/li&gt;
&lt;li&gt;Constraints work.&lt;/li&gt;
&lt;li&gt;Transactions work.&lt;/li&gt;
&lt;li&gt;Mappings work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same applies to queues, caches and external integrations.&lt;/p&gt;

&lt;p&gt;A repository can satisfy its TypeScript interface while generating an incorrect query. A database constraint may behave differently from an assumption encoded in a mock. A transaction may fail under conditions that a unit test never exercises.&lt;/p&gt;

&lt;p&gt;For example, if a requisition approval must update the requisition and create an audit record in the same transaction, an integration test should verify the actual database behavior.&lt;/p&gt;

&lt;p&gt;It should establish that both changes commit together and that a failure in either operation rolls back the relevant changes.&lt;/p&gt;

&lt;p&gt;This is particularly important when using an ORM. The ORM's type safety does not guarantee that every query, migration, relation, or transaction behaves correctly in the deployed database.&lt;/p&gt;

&lt;p&gt;Integration tests should use a real instance of the relevant dependency whenever its behavior is part of the contract being tested.&lt;/p&gt;

&lt;p&gt;For PostgreSQL, this often means running tests against an isolated test database, commonly provisioned through a disposable container.&lt;/p&gt;

&lt;p&gt;The goal is to verify the behavior of the real database rather than reproduce its behavior through a mock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the boundaries between layers
&lt;/h2&gt;

&lt;p&gt;A layered backend often separates controllers, application services, repositories, and infrastructure adapters.&lt;/p&gt;

&lt;p&gt;Each boundary introduces a contract.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Controller
      │
      ▼
Application Use Case
      │
      ▼
Repository Interface
      │
      ▼
PostgreSQL Adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller translates HTTP input into an application request. The use case applies business rules. The repository adapter translates application operations into database operations.&lt;/p&gt;

&lt;p&gt;Testing only the use case does not establish that the controller validates and maps input correctly.&lt;/p&gt;

&lt;p&gt;Likewise, testing only the repository does not establish that the HTTP endpoint returns the correct status code or response structure.&lt;/p&gt;

&lt;p&gt;The tests should reflect these responsibilities.&lt;/p&gt;

&lt;p&gt;For an API endpoint, relevant assertions may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invalid input produces the expected validation response.&lt;/li&gt;
&lt;li&gt;Authentication and authorization are enforced.&lt;/li&gt;
&lt;li&gt;The correct application use case is invoked.&lt;/li&gt;
&lt;li&gt;Business errors map to appropriate HTTP responses.&lt;/li&gt;
&lt;li&gt;Successful responses conform to the documented API contract.&lt;/li&gt;
&lt;li&gt;Internal implementation details are not unintentionally exposed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks help identify defects that are easy to miss when testing individual functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test architecture through dependency direction
&lt;/h2&gt;

&lt;p&gt;Testing also reveals whether the architecture has the boundaries it claims to have.&lt;/p&gt;

&lt;p&gt;If a business use case can only be tested by starting PostgreSQL, Redis, a queue, and several external services, it is worth examining its dependencies.&lt;/p&gt;

&lt;p&gt;That does not automatically mean the design is wrong. Some operations genuinely require integration testing.&lt;/p&gt;

&lt;p&gt;However, if a simple business rule is inseparable from infrastructure initialization, the application may have unnecessary coupling.&lt;/p&gt;

&lt;p&gt;A useful structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application Tests
       │
       ├── Fake Repository
       ├── Fake Message Publisher
       └── Fake External Service

Integration Tests
       │
       ├── Real PostgreSQL
       ├── Real Redis, where required
       └── Real Messaging Adapter, where required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first group verifies application behavior without requiring the complete infrastructure.&lt;/p&gt;

&lt;p&gt;The second group verifies that the infrastructure adapters honor their contracts.&lt;/p&gt;

&lt;p&gt;This separation allows business rules to be tested quickly while retaining confidence in the actual integrations.&lt;/p&gt;

&lt;p&gt;The objective is not to mock everything.&lt;/p&gt;

&lt;p&gt;It is to control dependencies where appropriate and exercise real dependencies where their behavior matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure testing
&lt;/h2&gt;

&lt;p&gt;A production-oriented test suite should also consider failure.&lt;/p&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database unavailable
Queue publish fails
Email provider fails
Duplicate request arrives
Transaction rolls back
External API times out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system's response to failure is part of its behavior.&lt;/p&gt;

&lt;p&gt;A system should not be considered reliable simply because its successful execution path works.&lt;/p&gt;

&lt;p&gt;Consider a background-job workflow. A job may be accepted successfully but fail during processing. The worker may retry it, encounter the same error repeatedly, or crash after performing an external side effect.&lt;/p&gt;

&lt;p&gt;Tests should verify the application's expected behavior in these situations.&lt;/p&gt;

&lt;p&gt;Depending on the workflow, that might mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A transient failure is retried according to policy.&lt;/li&gt;
&lt;li&gt;A permanent failure is surfaced for investigation.&lt;/li&gt;
&lt;li&gt;Duplicate processing does not repeat a protected business operation.&lt;/li&gt;
&lt;li&gt;A database transaction rolls back when a required operation fails.&lt;/li&gt;
&lt;li&gt;A failed external dependency does not leave the application in an invalid state.&lt;/li&gt;
&lt;li&gt;Errors are recorded with enough context to support diagnosis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests are especially valuable for asynchronous workflows because the producer, broker, and consumer do not share one execution context.&lt;/p&gt;

&lt;p&gt;The test should verify the actual recovery behavior rather than merely assert that an exception was thrown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test transactional consistency
&lt;/h2&gt;

&lt;p&gt;Database transactions and messaging introduce failure scenarios that ordinary happy-path tests may overlook.&lt;/p&gt;

&lt;p&gt;Suppose an application creates an order and records an outbox event in the same database transaction.&lt;/p&gt;

&lt;p&gt;A useful integration test verifies that both records are committed when the transaction succeeds and neither is committed when the transaction rolls back.&lt;/p&gt;

&lt;p&gt;The publisher can then be tested separately to verify that pending events are delivered and their publication state is updated appropriately.&lt;/p&gt;

&lt;p&gt;A further test should consider what happens when publication succeeds but the publisher fails before recording that success.&lt;/p&gt;

&lt;p&gt;The event may be published again during recovery.&lt;/p&gt;

&lt;p&gt;This is where idempotent consumers become important.&lt;/p&gt;

&lt;p&gt;Testing the transactional outbox therefore involves more than checking that an event was inserted into a table. It includes persistence, publication, retry, and duplicate-delivery behavior.&lt;/p&gt;

&lt;p&gt;The same principle applies to other business operations that coordinate multiple state changes.&lt;/p&gt;

&lt;p&gt;Tests should verify the consistency guarantees the architecture is intended to provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contract testing for replaceable infrastructure
&lt;/h2&gt;

&lt;p&gt;When an application supports multiple infrastructure providers, contract testing becomes particularly useful.&lt;/p&gt;

&lt;p&gt;For example, a messaging abstraction might have both a BullMQ adapter and an Azure Service Bus adapter.&lt;/p&gt;

&lt;p&gt;Both are expected to satisfy the application's message-publishing contract.&lt;/p&gt;

&lt;p&gt;Rather than testing each implementation against unrelated assumptions, define the shared behavior that every adapter must preserve.&lt;/p&gt;

&lt;p&gt;This may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Correct message serialization.&lt;/li&gt;
&lt;li&gt;Preservation of message identifiers and correlation metadata.&lt;/li&gt;
&lt;li&gt;Correct handling of publication failures.&lt;/li&gt;
&lt;li&gt;Appropriate error translation.&lt;/li&gt;
&lt;li&gt;Compliance with the application's delivery contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same reusable contract tests can then be run against each adapter where applicable.&lt;/p&gt;

&lt;p&gt;However, shared contract tests should not pretend that provider-specific features are identical.&lt;/p&gt;

&lt;p&gt;If one provider supports a particular scheduling or settlement behavior and another does not, that difference should be documented and tested explicitly.&lt;/p&gt;

&lt;p&gt;Contract testing helps establish that implementations honor the abstraction. It does not prove that two providers have identical operational guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  End-to-end tests should protect critical workflows
&lt;/h2&gt;

&lt;p&gt;Unit and integration tests do not eliminate the need for end-to-end testing.&lt;/p&gt;

&lt;p&gt;Some workflows cross enough boundaries that their combined behavior deserves verification.&lt;/p&gt;

&lt;p&gt;Consider a workflow that starts with an authenticated HTTP request, writes a business record, creates an outbox event, publishes a message, and eventually updates the processing status.&lt;/p&gt;

&lt;p&gt;An end-to-end test can verify that the complete workflow behaves correctly in a controlled environment.&lt;/p&gt;

&lt;p&gt;However, end-to-end tests are usually more expensive and more sensitive to environmental failures than unit tests.&lt;/p&gt;

&lt;p&gt;They should therefore focus on important business workflows rather than duplicate every assertion already covered by lower-level tests.&lt;/p&gt;

&lt;p&gt;For example, a requisition approval system might prioritize end-to-end tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Successful approval and audit recording.&lt;/li&gt;
&lt;li&gt;Rejection of an unauthorized approval attempt.&lt;/li&gt;
&lt;li&gt;Recovery from a failed asynchronous operation.&lt;/li&gt;
&lt;li&gt;Prevention of duplicate business effects.&lt;/li&gt;
&lt;li&gt;Correct handling of an invalid state transition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The objective is to establish confidence in the complete workflow, not to make every test exercise the entire application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid testing implementation details
&lt;/h2&gt;

&lt;p&gt;A test can pass consistently and still provide little confidence if it verifies the wrong thing.&lt;/p&gt;

&lt;p&gt;For example, asserting that a service calls a particular private helper may couple the test to the current implementation rather than the intended behavior.&lt;/p&gt;

&lt;p&gt;A harmless refactoring could then break the test without introducing a real defect.&lt;/p&gt;

&lt;p&gt;Prefer assertions about observable outcomes and meaningful contracts.&lt;/p&gt;

&lt;p&gt;For a business use case, that might mean verifying the resulting state, the emitted application message, or the error produced for an invalid operation.&lt;/p&gt;

&lt;p&gt;For a repository, it might mean verifying persisted records and database constraints.&lt;/p&gt;

&lt;p&gt;For an API, it might mean verifying the response status and payload.&lt;/p&gt;

&lt;p&gt;Mocks remain useful, especially for controlling failure scenarios. The problem is not mocking itself; it is treating a mock's configured behavior as proof that the real dependency behaves the same way.&lt;/p&gt;

&lt;p&gt;Tests should protect the system's intended behavior while allowing internal implementations to evolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage is a signal, not the objective
&lt;/h2&gt;

&lt;p&gt;Code coverage can help identify untested parts of a codebase.&lt;/p&gt;

&lt;p&gt;It cannot, by itself, establish that the important business scenarios have been tested.&lt;/p&gt;

&lt;p&gt;A test suite can execute every line of a function without verifying the correctness of its decisions.&lt;/p&gt;

&lt;p&gt;Likewise, a system with a lower coverage percentage may have strong tests around its most critical business rules and failure boundaries.&lt;/p&gt;

&lt;p&gt;Coverage is useful when interpreted alongside other evidence.&lt;/p&gt;

&lt;p&gt;I would pay particular attention to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Critical business rules and state transitions.&lt;/li&gt;
&lt;li&gt;Authorization and access-control boundaries.&lt;/li&gt;
&lt;li&gt;Transactional consistency.&lt;/li&gt;
&lt;li&gt;Data integrity and database constraints.&lt;/li&gt;
&lt;li&gt;Message delivery and duplicate-processing behavior.&lt;/li&gt;
&lt;li&gt;Recovery from external dependency failures.&lt;/li&gt;
&lt;li&gt;Important API contracts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These areas often matter more to production confidence than achieving a particular percentage without considering what the tests actually establish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Testing should follow architectural boundaries.&lt;/p&gt;

&lt;p&gt;The goal is not simply to achieve a percentage.&lt;/p&gt;

&lt;p&gt;The goal is to establish confidence that the system behaves correctly at each important boundary.&lt;/p&gt;

&lt;p&gt;Unit tests help verify business rules. Integration tests verify that real components work together. Contract tests protect agreements between independently implemented components. End-to-end tests establish confidence in critical workflows.&lt;/p&gt;

&lt;p&gt;Each has a role.&lt;/p&gt;

&lt;p&gt;A well-designed test suite should make it possible to change an infrastructure implementation, refactor application logic, or introduce a new integration without unnecessarily increasing the risk of regressions.&lt;/p&gt;

&lt;p&gt;That is the purpose of testing backend architecture, not just functions.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>node</category>
      <category>software</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Transactions, Events and the Problem of Reliable Side Effects</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:32:21 +0000</pubDate>
      <link>https://dev.to/topzyray/transactions-events-and-the-problem-of-reliable-side-effects-4dmb</link>
      <guid>https://dev.to/topzyray/transactions-events-and-the-problem-of-reliable-side-effects-4dmb</guid>
      <description>&lt;p&gt;&lt;em&gt;"Understand the dual-write problem and how transactional outbox patterns, idempotent consumers, retries, and recovery improve event reliability."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A common backend operation looks simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Update database
      +
Publish event
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difficulty appears when one succeeds and the other fails.&lt;/p&gt;

&lt;p&gt;This is a problem I consider when designing backend systems that coordinate database transactions with asynchronous processing, external services, or other independently operating components.&lt;/p&gt;

&lt;p&gt;The challenge is not simply how to save a record or publish a message.&lt;/p&gt;

&lt;p&gt;It is how to ensure that the system remains consistent when failures occur between those operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dual-write problem
&lt;/h2&gt;

&lt;p&gt;Suppose the application does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Save order
2. Publish OrderCreated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If step 1 succeeds and step 2 fails, the database contains the order but downstream systems never receive the event.&lt;/p&gt;

&lt;p&gt;The order may exist, but the systems responsible for inventory, notifications, reporting, or other follow-up operations may never learn that it was created.&lt;/p&gt;

&lt;p&gt;Reverse the order and another failure mode appears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Publish event
2. Save order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the database operation fails after the event has been published, consumers may process an event for a transaction that never committed.&lt;/p&gt;

&lt;p&gt;The problem exists because the application is trying to coordinate two independent operations without a shared atomic commit.&lt;/p&gt;

&lt;p&gt;This is commonly known as the &lt;strong&gt;dual-write problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The two operations may each be reliable on their own. That does not make the combination reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions do not automatically solve messaging
&lt;/h2&gt;

&lt;p&gt;A database transaction can provide atomicity within the database.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEGIN
  │
  ├── Insert Order
  ├── Insert Order Items
  └── Update Inventory
  │
COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an operation fails before the transaction commits, the database can roll back the changes covered by that transaction.&lt;/p&gt;

&lt;p&gt;This is valuable because related business changes remain consistent.&lt;/p&gt;

&lt;p&gt;However, a database transaction cannot automatically make an external message broker part of that same transaction.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;messageBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OrderCreated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this appears to combine the database change and event publication into one operation.&lt;/p&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;The message broker does not automatically participate in the PostgreSQL transaction.&lt;/p&gt;

&lt;p&gt;If the message is published successfully but the database transaction later rolls back, consumers may process an event for an order that does not exist.&lt;/p&gt;

&lt;p&gt;If the database commits but the application encounters a failure while publishing, the order may exist without the corresponding event.&lt;/p&gt;

&lt;p&gt;Holding a database transaction open while calling an external service also increases transaction duration and can introduce unnecessary latency, resource contention, and failure dependencies.&lt;/p&gt;

&lt;p&gt;The important distinction is that &lt;strong&gt;database atomicity does not imply atomicity across independent systems&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transactional outbox pattern
&lt;/h2&gt;

&lt;p&gt;This is where patterns such as the transactional outbox become useful.&lt;/p&gt;

&lt;p&gt;The idea is straightforward: instead of attempting to publish an event directly as part of the database transaction, persist the intent to publish it in the same transaction as the business change.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database Transaction
      │
      ├── Business Data
      │
      └── Outbox Event
              │
              ▼
          COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are committed together.&lt;/p&gt;

&lt;p&gt;A separate process can then publish the outbox event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outbox
  │
  ▼
Publisher
  │
  ▼
Message Bus
  │
  ▼
Consumers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the transaction rolls back, neither the business change nor its outbox event is committed.&lt;/p&gt;

&lt;p&gt;If the transaction commits, the outbox record remains available for publication, even if the messaging infrastructure is temporarily unavailable.&lt;/p&gt;

&lt;p&gt;The publisher can retry delivery without requiring the original business operation to run again.&lt;/p&gt;

&lt;p&gt;This changes the consistency problem.&lt;/p&gt;

&lt;p&gt;Instead of trying to make a database write and a broker publication happen atomically, the application makes the business change and the intent to publish atomic. A separate process is responsible for delivering that intent.&lt;/p&gt;

&lt;p&gt;The message may be published later, but the system has a durable record that publication is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical implementation
&lt;/h2&gt;

&lt;p&gt;In a PostgreSQL-backed TypeScript application, the business record and outbox event can be persisted through the same Prisma transaction.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outboxEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OrderCreated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;aggregateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example assumes an outbox model containing the corresponding fields.&lt;/p&gt;

&lt;p&gt;The significant detail is that both operations use the same transaction client, &lt;code&gt;tx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If either operation fails, the transaction does not commit either change.&lt;/p&gt;

&lt;p&gt;The outbox record might contain fields such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;eventId&lt;/code&gt;: A stable identifier for the event.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eventType&lt;/code&gt;: The type of event to publish.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aggregateId&lt;/code&gt;: The identifier of the business entity associated with the event.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload&lt;/code&gt;: The serialized event data.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;: The publication state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createdAt&lt;/code&gt;: When the event was recorded.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;publishedAt&lt;/code&gt;: When successful publication was recorded, if applicable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attempts&lt;/code&gt;: The number of publication attempts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lastError&lt;/code&gt;: Diagnostic information about the most recent failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact schema depends on the application's requirements.&lt;/p&gt;

&lt;p&gt;The outbox is not simply a logging table. It represents durable work that remains to be delivered.&lt;/p&gt;

&lt;p&gt;Its lifecycle, retention policy, indexing strategy, and recovery behavior therefore deserve the same attention as other operational components.&lt;/p&gt;

&lt;h2&gt;
  
  
  The publisher introduces another failure boundary
&lt;/h2&gt;

&lt;p&gt;The transactional outbox solves the original dual-write problem, but it does not eliminate every failure mode.&lt;/p&gt;

&lt;p&gt;Consider the publisher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Read pending outbox event
2. Publish event
3. Mark event as published
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens if the message broker accepts the event but the publisher crashes before step 3?&lt;/p&gt;

&lt;p&gt;The outbox record still appears pending.&lt;/p&gt;

&lt;p&gt;When the publisher retries, it may publish the same event again.&lt;/p&gt;

&lt;p&gt;This means a transactional outbox generally supports &lt;strong&gt;reliable, retryable publication with possible duplicate delivery&lt;/strong&gt;, not automatic exactly-once processing across the database and broker.&lt;/p&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;

&lt;p&gt;A robust implementation should assume that an event may be delivered more than once and design its consumers accordingly.&lt;/p&gt;

&lt;p&gt;For example, a consumer can use the stable event identifier to detect duplicate processing. For database-backed business effects, a unique constraint on the processed-event identifier can help prevent the same logical event from being applied twice.&lt;/p&gt;

&lt;p&gt;However, deduplication and external side effects require careful handling. Recording an event as processed and performing an external action are not automatically atomic either.&lt;/p&gt;

&lt;p&gt;The outbox moves the reliability boundary. It does not remove the need to reason about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consumer idempotency still matters
&lt;/h2&gt;

&lt;p&gt;Suppose an &lt;code&gt;OrderCreated&lt;/code&gt; event triggers an inventory reservation.&lt;/p&gt;

&lt;p&gt;A consumer might receive the same event twice because the publisher retried it or the broker redelivered it.&lt;/p&gt;

&lt;p&gt;Without appropriate safeguards, the inventory reservation could be applied twice.&lt;/p&gt;

&lt;p&gt;A simplified processing design looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive Event
     │
     ▼
Check Event Identity
     │
     ├── Already Processed
     │       │
     │       ▼
     │   Skip Duplicate
     │
     └── Not Processed
             │
             ▼
       Apply Business Change
             │
             ▼
       Record Event Identity
             │
             ▼
          COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the business change and processed-event record belong to the same database, they can often be committed in one transaction.&lt;/p&gt;

&lt;p&gt;A uniqueness constraint on the event identifier provides an additional safeguard against concurrent duplicate deliveries.&lt;/p&gt;

&lt;p&gt;If processing requires an external API call, however, the consumer must consider the possibility of a crash between the external operation and the local completion record.&lt;/p&gt;

&lt;p&gt;Depending on the operation, a provider-supported idempotency key, a durable workflow, reconciliation, or another coordination mechanism may be necessary.&lt;/p&gt;

&lt;p&gt;The correct approach depends on the business consequence of repeating the operation.&lt;/p&gt;

&lt;p&gt;For example, generating a report twice may be tolerable. Applying the same financial adjustment twice may not be.&lt;/p&gt;

&lt;p&gt;Idempotency is therefore not just a messaging concern. It is a property that should be considered when designing the business operation itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling versus change data capture
&lt;/h2&gt;

&lt;p&gt;There is more than one way to publish outbox events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Polling
&lt;/h3&gt;

&lt;p&gt;A background publisher periodically queries the outbox table for pending events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Outbox Table
     │
     ▼
Poll Pending Events
     │
     ▼
Publish to Broker
     │
     ▼
Update Publication State
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Polling is relatively straightforward to implement and operate.&lt;/p&gt;

&lt;p&gt;However, the implementation must account for concurrent publishers, batch sizes, event ordering, retry behavior, and the possibility of multiple workers attempting to claim the same record.&lt;/p&gt;

&lt;p&gt;Database locking, appropriately designed claim operations, or other coordination mechanisms can help prevent unnecessary contention.&lt;/p&gt;

&lt;p&gt;The polling interval also affects publication latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change data capture
&lt;/h3&gt;

&lt;p&gt;Another approach is to use change data capture (CDC) to observe committed database changes and feed outbox records into a messaging pipeline.&lt;/p&gt;

&lt;p&gt;This can reduce the need for an application-managed polling loop and may be appropriate for systems with higher throughput or existing streaming infrastructure.&lt;/p&gt;

&lt;p&gt;However, CDC introduces its own operational requirements, including connector management, replication-log retention, schema evolution, monitoring, and recovery.&lt;/p&gt;

&lt;p&gt;Neither approach is universally better.&lt;/p&gt;

&lt;p&gt;Polling can be a good starting point for a system with moderate throughput and straightforward operational requirements. CDC may be more suitable when the scale, latency requirements, or existing infrastructure justify the additional complexity.&lt;/p&gt;

&lt;p&gt;The choice should follow the workload rather than the popularity of the pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event ordering and concurrent publishers
&lt;/h2&gt;

&lt;p&gt;Reliable publication does not automatically guarantee that every consumer will observe events in the order the business produced them.&lt;/p&gt;

&lt;p&gt;Suppose an order produces these events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
OrderConfirmed
OrderDispatched
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If multiple publishers process pending records concurrently, or the broker distributes messages across partitions or sessions, downstream processing may observe a different order unless ordering is explicitly supported and configured.&lt;/p&gt;

&lt;p&gt;For many workflows, this is manageable because consumers validate the current business state or process events independently.&lt;/p&gt;

&lt;p&gt;For workflows that require ordering, the design may need to preserve order per aggregate, use a stable partition or session key where supported, or prevent later events from overtaking earlier events for the same entity.&lt;/p&gt;

&lt;p&gt;The requirement should be explicit.&lt;/p&gt;

&lt;p&gt;Global ordering can constrain throughput unnecessarily. Per-aggregate ordering is often sufficient when events concern independent business entities.&lt;/p&gt;

&lt;p&gt;The outbox schema and publisher strategy should reflect that distinction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries, dead-lettering, and operational recovery
&lt;/h2&gt;

&lt;p&gt;A publisher needs a clear response to failures.&lt;/p&gt;

&lt;p&gt;Temporary broker unavailability may justify retries with exponential backoff. Invalid message contracts or permanently rejected messages may require investigation instead of endless retries.&lt;/p&gt;

&lt;p&gt;A production design should establish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which failures are retryable.&lt;/li&gt;
&lt;li&gt;How retry delays are calculated.&lt;/li&gt;
&lt;li&gt;Whether retries have a maximum or continue until resolved.&lt;/li&gt;
&lt;li&gt;How permanently failing events are isolated.&lt;/li&gt;
&lt;li&gt;How operators inspect and replay failed events.&lt;/li&gt;
&lt;li&gt;How long published records are retained.&lt;/li&gt;
&lt;li&gt;How publication lag is monitored.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A dead-letter mechanism can isolate messages that cannot be processed automatically, but moving a message to a dead-letter queue does not resolve the underlying business problem.&lt;/p&gt;

&lt;p&gt;Likewise, deleting a failed outbox record may remove the evidence that a required business event was never delivered.&lt;/p&gt;

&lt;p&gt;Recovery should be deliberate and auditable.&lt;/p&gt;

&lt;p&gt;Useful operational metrics include the number of pending events, the age of the oldest pending event, publication failures, retry counts, and the time between event creation and successful publication.&lt;/p&gt;

&lt;p&gt;These signals reveal whether the asynchronous pipeline is keeping up with the business workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  This connects to previous decisions
&lt;/h2&gt;

&lt;p&gt;This is why database architecture and messaging architecture cannot always be designed independently.&lt;/p&gt;

&lt;p&gt;The moment a business operation produces an external side effect, consistency becomes an architectural concern.&lt;/p&gt;

&lt;p&gt;It also connects to how I approach application-level messaging boundaries.&lt;/p&gt;

&lt;p&gt;An application should not need to know whether its event is eventually published through BullMQ, Azure Service Bus, or another messaging infrastructure. However, the application still needs a reliable way to record the obligation to publish that event.&lt;/p&gt;

&lt;p&gt;The outbox provides that durable boundary between the database transaction and the messaging infrastructure.&lt;/p&gt;

&lt;p&gt;The publisher can then implement the transport-specific behavior, while consumers handle idempotency and the business effects associated with the event.&lt;/p&gt;

&lt;p&gt;These responsibilities are separate, but they must work together.&lt;/p&gt;

&lt;p&gt;Not every operation needs an outbox. A best-effort cache refresh may not justify one. A business workflow that must not silently lose an event may.&lt;/p&gt;

&lt;p&gt;The decision should reflect the consequences of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Distributed systems often fail at the boundaries between systems.&lt;/p&gt;

&lt;p&gt;Understanding those boundaries is more valuable than memorizing patterns.&lt;/p&gt;

&lt;p&gt;The outbox pattern is useful because it addresses one specific consistency problem: reliably connecting database state changes with asynchronous messaging.&lt;/p&gt;

&lt;p&gt;It does not eliminate duplicate delivery, guarantee exactly-once execution, or make every external side effect atomic.&lt;/p&gt;

&lt;p&gt;It gives the application a durable record of what must be published and a mechanism for recovering when the messaging infrastructure is unavailable.&lt;/p&gt;

&lt;p&gt;That is the distinction that matters when designing reliable backend systems.&lt;/p&gt;

&lt;p&gt;The goal is not to add patterns everywhere.&lt;/p&gt;

&lt;p&gt;It is to understand which failure a pattern addresses, which guarantees it provides, and which responsibilities still belong elsewhere in the architecture.&lt;/p&gt;

</description>
      <category>backenddevelopment</category>
      <category>distributedsystems</category>
      <category>node</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Designing Background Jobs Without Infrastructure Coupling</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Wed, 21 Oct 2026 09:34:21 +0000</pubDate>
      <link>https://dev.to/topzyray/designing-background-jobs-without-infrastructure-coupling-khk</link>
      <guid>https://dev.to/topzyray/designing-background-jobs-without-infrastructure-coupling-khk</guid>
      <description>&lt;p&gt;&lt;em&gt;"Use messaging boundaries to keep application workflows independent of queue providers while preserving delivery guarantees, idempotency, and job semantics."&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Background processing is an important part of backend systems.&lt;/p&gt;

&lt;p&gt;Email delivery, notifications, reports, imports and other long-running operations do not always belong inside the HTTP request.&lt;/p&gt;

&lt;p&gt;Some operations are too expensive to execute synchronously. Others depend on external services that may be slow or temporarily unavailable. In many cases, the API should accept a request, record the necessary business state, and allow a worker to complete the operation independently.&lt;/p&gt;

&lt;p&gt;The architectural question is how to introduce asynchronous processing without coupling the entire application to one queue technology.&lt;/p&gt;

&lt;h2&gt;
  
  
  The direct approach
&lt;/h2&gt;

&lt;p&gt;A service might do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;send-email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works.&lt;/p&gt;

&lt;p&gt;For a small application, it may even be sufficient.&lt;/p&gt;

&lt;p&gt;However, as the application grows, the queue client can start appearing throughout application services, controllers, and business workflows.&lt;/p&gt;

&lt;p&gt;The problem is not the method call itself. The problem is the dependency it introduces.&lt;/p&gt;

&lt;p&gt;If application services depend directly on BullMQ, for example, they become aware of concepts and behaviors associated with that infrastructure. Switching to Azure Service Bus may then require changes beyond the infrastructure layer.&lt;/p&gt;

&lt;p&gt;The application may also become coupled to provider-specific options such as job priorities, delay settings, retry configuration, and scheduling APIs.&lt;/p&gt;

&lt;p&gt;That is a dependency-management problem, not merely a queue-management problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduce a messaging boundary
&lt;/h2&gt;

&lt;p&gt;A better boundary can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MessageBus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DomainMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application can then say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;messageBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EmailRequested&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The infrastructure decides how to deliver it.&lt;/p&gt;

&lt;p&gt;This follows the dependency inversion principle: application-level code depends on an abstraction, while concrete messaging infrastructure implements that abstraction.&lt;/p&gt;

&lt;p&gt;The business operation expresses what needs to happen. The infrastructure determines how the message is transported.&lt;/p&gt;

&lt;p&gt;A more complete TypeScript contract might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MessageEnvelope&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MessageBus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageEnvelope&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The envelope provides a stable place for message identity, schema versioning, timestamps, and request correlation.&lt;/p&gt;

&lt;p&gt;These fields become useful when investigating failures across HTTP requests, background workers, and external services.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; field also provides a foundation for evolving message contracts without assuming that every producer and consumer will be deployed simultaneously.&lt;/p&gt;

&lt;p&gt;One important distinction: a message such as &lt;code&gt;EmailRequested&lt;/code&gt; is generally an application or integration message, not necessarily a domain event. A domain event describes something meaningful that has happened in the business, such as &lt;code&gt;OrderConfirmed&lt;/code&gt;. A command or request asks for an operation to be performed.&lt;/p&gt;

&lt;p&gt;These concepts can share transport infrastructure, but they should retain their distinct meanings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate publishing from processing
&lt;/h2&gt;

&lt;p&gt;A messaging boundary is only one part of the architecture.&lt;/p&gt;

&lt;p&gt;The consumer also needs a boundary between message delivery and business processing.&lt;/p&gt;

&lt;p&gt;A simplified design looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Request
     │
     ▼
Application Service
     │
     ▼
MessageBus Interface
     │
     ▼
Messaging Adapter
     │
     ├── BullMQ / Redis
     │
     └── Azure Service Bus
                  │
                  ▼
              Consumer
                  │
                  ▼
          Application Handler
                  │
                  ▼
           Business Operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer and consumer can now evolve independently of the underlying transport.&lt;/p&gt;

&lt;p&gt;The HTTP application does not need to know how a worker receives messages. Likewise, the business handler does not need to know whether the message arrived through a Redis-backed queue or a cloud messaging service.&lt;/p&gt;

&lt;p&gt;For example, a report-generation handler should be concerned with validating the request, generating the report, storing the output, and recording the result.&lt;/p&gt;

&lt;p&gt;It should not need to know how BullMQ acknowledges a job or how Azure Service Bus settles a message.&lt;/p&gt;

&lt;p&gt;Those concerns belong in the infrastructure adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I care about this
&lt;/h2&gt;

&lt;p&gt;I have specifically been designing systems where the messaging infrastructure should not be permanently tied to one provider.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MessageBus
   │
   ├── BullMQ / Redis
   │
   └── Azure Service Bus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business operation should not have to change simply because the transport changes.&lt;/p&gt;

&lt;p&gt;This is particularly useful when an application begins with a lightweight Redis-backed queue but later requires managed messaging infrastructure, different operational capabilities, or integration with other enterprise systems.&lt;/p&gt;

&lt;p&gt;However, provider independence should be an architectural objective rather than a promise that switching providers will always be effortless.&lt;/p&gt;

&lt;p&gt;The application should isolate provider-specific dependencies while explicitly defining the capabilities it requires.&lt;/p&gt;

&lt;p&gt;A transport change may still require infrastructure configuration, deployment changes, operational adjustments, and changes to message routing.&lt;/p&gt;

&lt;p&gt;The purpose of the boundary is to prevent those changes from unnecessarily spreading through the business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the abstraction around application needs
&lt;/h2&gt;

&lt;p&gt;An abstraction should describe the capabilities the application actually needs.&lt;/p&gt;

&lt;p&gt;For example, if the application only needs to publish background work, a small interface may be sufficient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;BackgroundJobPublisher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageEnvelope&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the application needs delayed delivery, scheduling, or explicit routing, those capabilities should be introduced deliberately.&lt;/p&gt;

&lt;p&gt;They should not be added merely because one provider exposes them.&lt;/p&gt;

&lt;p&gt;Consider these examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Application-level intent&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jobPublisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Provider-specific behavior leaking into application code&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bullQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generate-report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exponential&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;removeOnComplete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second example is not inherently bad. Those options are useful when configuring BullMQ.&lt;/p&gt;

&lt;p&gt;The architectural problem appears when business services must understand and reproduce those settings across multiple providers.&lt;/p&gt;

&lt;p&gt;Instead, infrastructure configuration should translate application-level policies into provider-specific options.&lt;/p&gt;

&lt;p&gt;For example, an application may define a retry policy for transient report-generation failures. The BullMQ adapter can translate that policy into its retry configuration, while an Azure Service Bus adapter can implement the appropriate delivery, redelivery, and dead-letter behavior supported by that platform.&lt;/p&gt;

&lt;p&gt;The abstraction should remain small enough to be understandable and explicit enough to preserve the guarantees the application needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  But abstraction must preserve semantics
&lt;/h2&gt;

&lt;p&gt;A queue abstraction cannot pretend that every messaging platform behaves identically.&lt;/p&gt;

&lt;p&gt;The design must account for the semantics the application actually needs.&lt;/p&gt;

&lt;p&gt;This includes questions around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retries.&lt;/li&gt;
&lt;li&gt;Ordering.&lt;/li&gt;
&lt;li&gt;Acknowledgement and settlement.&lt;/li&gt;
&lt;li&gt;Dead-lettering.&lt;/li&gt;
&lt;li&gt;Idempotency.&lt;/li&gt;
&lt;li&gt;Delivery guarantees.&lt;/li&gt;
&lt;li&gt;Delayed delivery and scheduling.&lt;/li&gt;
&lt;li&gt;Message expiration.&lt;/li&gt;
&lt;li&gt;Concurrency and backpressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These differences matter because two providers may expose similar APIs while providing different operational guarantees.&lt;/p&gt;

&lt;p&gt;For example, BullMQ manages jobs through Redis and provides job-oriented capabilities such as retries, delays, and concurrency controls.&lt;/p&gt;

&lt;p&gt;Azure Service Bus provides managed messaging capabilities, including message settlement, delivery-count handling, and dead-letter queues.&lt;/p&gt;

&lt;p&gt;A successful method call to publish a message also does not mean the business operation has completed. It means the message was accepted according to the publishing mechanism's guarantees.&lt;/p&gt;

&lt;p&gt;The consumer may execute later, fail, retry, or encounter a permanent error.&lt;/p&gt;

&lt;p&gt;The application must define what success means at each stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivery guarantees and idempotency
&lt;/h2&gt;

&lt;p&gt;Distributed messaging systems can deliver the same logical work more than once.&lt;/p&gt;

&lt;p&gt;A worker may complete an external operation and then fail before acknowledging the message. The infrastructure may redeliver the message because it cannot establish that processing completed successfully.&lt;/p&gt;

&lt;p&gt;This means an application should not assume that a job handler executes exactly once merely because its queue has retries or acknowledgement support.&lt;/p&gt;

&lt;p&gt;Consider a notification workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Message Received
       │
       ▼
Send Notification
       │
       ▼
Record Completion
       │
       ▼
Acknowledge Message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the process crashes after sending the notification but before recording completion, a retry could send the notification again.&lt;/p&gt;

&lt;p&gt;An idempotent consumer is designed to handle repeated delivery without repeating the business effect unnecessarily.&lt;/p&gt;

&lt;p&gt;One approach is to assign a stable message identifier and record processed messages in a durable store with a suitable uniqueness constraint.&lt;/p&gt;

&lt;p&gt;However, a deduplication record alone does not make every external side effect atomic. If sending an email and recording its completion are separate operations, a crash can still occur between them.&lt;/p&gt;

&lt;p&gt;For external effects, the design may need provider-supported idempotency keys, durable delivery records, reconciliation, or a workflow that explicitly handles uncertain outcomes.&lt;/p&gt;

&lt;p&gt;The correct solution depends on the business operation.&lt;/p&gt;

&lt;p&gt;For example, duplicate report-generation work may be acceptable if the output is safely replaceable. Duplicate financial transfers are not.&lt;/p&gt;

&lt;p&gt;The messaging abstraction must not hide these differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database transaction problem
&lt;/h2&gt;

&lt;p&gt;One of the most important failure cases appears when an application writes to a database and publishes a message as part of the same business operation.&lt;/p&gt;

&lt;p&gt;Consider this sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Save Order to PostgreSQL
2. Publish OrderConfirmed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens if the database transaction commits successfully but the message publication fails?&lt;/p&gt;

&lt;p&gt;The order exists, but downstream consumers may never receive the event.&lt;/p&gt;

&lt;p&gt;Reversing the order does not eliminate the problem. The message might be published successfully while the database transaction subsequently rolls back.&lt;/p&gt;

&lt;p&gt;This is the dual-write problem.&lt;/p&gt;

&lt;p&gt;For business operations that require reliable event publication, the transactional outbox pattern is a common solution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database Transaction
        │
        ├── Save Business Record
        │
        └── Save Outbox Message
                  │
                  ▼
             Transaction Commit
                  │
                  ▼
            Outbox Publisher
                  │
                  ▼
              Message Bus
                  │
                  ▼
               Consumer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business record and its outbox message are committed in the same database transaction.&lt;/p&gt;

&lt;p&gt;A separate publisher reads pending outbox records and publishes them to the configured messaging infrastructure. After successful publication, it records the appropriate delivery state.&lt;/p&gt;

&lt;p&gt;The publisher may still crash after sending a message but before marking the outbox record as published. Therefore, duplicate publication remains possible, and consumers should be designed accordingly.&lt;/p&gt;

&lt;p&gt;The outbox pattern does not create a distributed transaction between PostgreSQL and the queue. It makes the intent to publish durable and recoverable.&lt;/p&gt;

&lt;p&gt;It is particularly useful for order processing, approvals, financial workflows, and other operations where losing an event would create an inconsistent business state.&lt;/p&gt;

&lt;p&gt;For a best-effort cache refresh or noncritical notification, the added complexity may not be justified. The reliability requirement should determine the pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing is not the same as processing
&lt;/h2&gt;

&lt;p&gt;A clean messaging boundary should distinguish the acceptance of a message from the completion of its work.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;messageBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should not imply that the email has been delivered or the report has been generated.&lt;/p&gt;

&lt;p&gt;It should mean that the publishing operation completed according to the contract defined by the adapter.&lt;/p&gt;

&lt;p&gt;The application may return a response such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"messageId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"msg_123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact response depends on the API contract.&lt;/p&gt;

&lt;p&gt;If the client needs to know when processing finishes, the application needs an additional mechanism, such as a persisted job-status resource, a notification, or a polling endpoint.&lt;/p&gt;

&lt;p&gt;This distinction prevents an asynchronous API from promising synchronous completion.&lt;/p&gt;

&lt;p&gt;It also helps separate three different concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Acceptance:&lt;/strong&gt; Was the work successfully submitted?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution:&lt;/strong&gt; Did a worker process it successfully?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business completion:&lt;/strong&gt; Was the intended business outcome achieved and recorded?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those states may occur at different times and should not be conflated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid building a generic messaging framework
&lt;/h2&gt;

&lt;p&gt;There is a temptation to create an abstraction that supports every conceivable queue feature.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;UniversalQueue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;acknowledge&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;deadLetter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;changePriority&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can become a lowest-common-denominator abstraction that either exposes provider-specific details everywhere or fails to express the capabilities of individual providers accurately.&lt;/p&gt;

&lt;p&gt;Not every messaging technology supports the same operations in the same way.&lt;/p&gt;

&lt;p&gt;A better approach is to define a small core contract and introduce additional interfaces only when the application genuinely requires those capabilities.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MessagePublisher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageEnvelope&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ScheduledMessagePublisher&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;MessagePublisher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;publishAt&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageEnvelope&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;scheduledAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even this extension should be introduced only when scheduling is a real application requirement.&lt;/p&gt;

&lt;p&gt;If scheduling semantics differ between providers, the contract must define what &lt;code&gt;scheduledAt&lt;/code&gt; means, what happens when a message is late, and how cancellation or rescheduling works.&lt;/p&gt;

&lt;p&gt;A smaller abstraction with explicit limitations is often more maintainable than a universal interface that promises capabilities it cannot consistently deliver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organize the TypeScript implementation around boundaries
&lt;/h2&gt;

&lt;p&gt;In a TypeScript backend, one possible feature-oriented structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── modules/
│   └── notifications/
│       ├── application/
│       │   ├── ports/
│       │   │   └── message-publisher.ts
│       │   └── use-cases/
│       │       └── request-notification.ts
│       ├── domain/
│       │   └── notification.ts
│       └── infrastructure/
│           ├── messaging/
│           │   ├── bullmq-message-publisher.ts
│           │   └── service-bus-message-publisher.ts
│           └── consumers/
│               └── notification-consumer.ts
├── shared/
│   └── messaging/
│       ├── message-envelope.ts
│       └── message-registry.ts
└── bootstrap/
    └── messaging.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an illustrative structure rather than a mandatory folder convention.&lt;/p&gt;

&lt;p&gt;The application layer owns the port it needs. Infrastructure adapters implement that port. The bootstrap layer selects and configures the implementation.&lt;/p&gt;

&lt;p&gt;For example, dependency injection can bind the application interface to a BullMQ-backed implementation in one deployment and an Azure Service Bus-backed implementation in another.&lt;/p&gt;

&lt;p&gt;The consumer's business handler can remain independent of the transport, while a provider-specific consumer adapter handles delivery, settlement, retries, and error translation.&lt;/p&gt;

&lt;p&gt;Message schemas should also be validated at the system boundary. TypeScript types disappear at runtime, and a message may have been produced by an older application version or another service.&lt;/p&gt;

&lt;p&gt;A typed interface alone is not sufficient to guarantee that a received message has a valid payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the boundary
&lt;/h2&gt;

&lt;p&gt;A messaging abstraction also makes application services easier to test.&lt;/p&gt;

&lt;p&gt;A unit test can replace the real message publisher with a fake implementation that records published messages.&lt;/p&gt;

&lt;p&gt;The test can then verify that the application requests the expected operation without requiring Redis, Azure credentials, or a running worker.&lt;/p&gt;

&lt;p&gt;Separate integration tests should verify that each adapter correctly maps application contracts to provider behavior.&lt;/p&gt;

&lt;p&gt;For example, adapter tests can cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Successful publication.&lt;/li&gt;
&lt;li&gt;Transient infrastructure failures.&lt;/li&gt;
&lt;li&gt;Invalid or incompatible messages.&lt;/li&gt;
&lt;li&gt;Retry and dead-letter behavior.&lt;/li&gt;
&lt;li&gt;Duplicate delivery.&lt;/li&gt;
&lt;li&gt;Serialization and schema validation.&lt;/li&gt;
&lt;li&gt;Connection loss and recovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to test both the application-level intent and the infrastructure-level guarantees.&lt;/p&gt;

&lt;p&gt;A passing unit test does not prove that a queue configuration is reliable in production. Likewise, an integration test against one provider does not establish that another provider has identical semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The goal is not to create a generic "queue wrapper."&lt;/p&gt;

&lt;p&gt;The goal is to create a meaningful application-level messaging boundary while keeping infrastructure replaceable.&lt;/p&gt;

&lt;p&gt;That boundary should protect business logic from provider-specific APIs without hiding the operational realities of asynchronous processing.&lt;/p&gt;

&lt;p&gt;Retries still require careful policies. Consumers still need idempotency. Critical database changes may require an outbox. Delivery guarantees must be understood rather than assumed.&lt;/p&gt;

&lt;p&gt;A well-designed abstraction does not make these problems disappear.&lt;/p&gt;

&lt;p&gt;It gives the application a clear place to define its requirements and gives the infrastructure a clear responsibility for implementing them.&lt;/p&gt;

&lt;p&gt;That is the difference between simply hiding a queue client behind an interface and designing a backend that can evolve its messaging infrastructure without unnecessarily rewriting its business logic.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>typescript</category>
      <category>node</category>
      <category>devops</category>
    </item>
    <item>
      <title>Redis Is More Than a Cache</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Tue, 20 Oct 2026 09:07:54 +0000</pubDate>
      <link>https://dev.to/topzyray/redis-is-more-than-a-cache-1ohl</link>
      <guid>https://dev.to/topzyray/redis-is-more-than-a-cache-1ohl</guid>
      <description>&lt;p&gt;&lt;em&gt;Explore Redis for caching, rate limiting, sessions, coordination, messaging, and background jobs, with attention to trade-offs and failure modes&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Redis is frequently introduced to developers as a caching system.&lt;/p&gt;

&lt;p&gt;That is only one of its uses.&lt;/p&gt;

&lt;p&gt;In backend architecture, Redis can participate in several different patterns, including caching, session management, rate limiting, distributed coordination, real-time messaging, and background-job infrastructure.&lt;/p&gt;

&lt;p&gt;Its usefulness comes from more than simply storing data in memory. Redis provides data structures and operations that can help applications solve specific problems without repeatedly querying a database or performing expensive computations.&lt;/p&gt;

&lt;p&gt;However, using Redis effectively requires understanding what each pattern is responsible for, what guarantees it provides, and what happens when things fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache and queue are different responsibilities
&lt;/h2&gt;

&lt;p&gt;A cache answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I retrieve this expensive result without recomputing it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A queue answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I defer this piece of work and process it asynchronously?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They are different problems.&lt;/p&gt;

&lt;p&gt;A technology can support both without making the concepts identical.&lt;/p&gt;

&lt;p&gt;For example, an application might use Redis to cache a user's profile while also using BullMQ to schedule email delivery, report generation, or other background tasks.&lt;/p&gt;

&lt;p&gt;The cached profile is optimized for repeated reads. The queued job represents work that must be executed, retried, or scheduled according to the queue's processing rules.&lt;/p&gt;

&lt;p&gt;These responsibilities should remain separate in the application architecture, even when they share the same Redis infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read-through caching
&lt;/h2&gt;

&lt;p&gt;A useful cache pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request
  │
  ▼
Cache
  │
  ├── Hit → Return
  │
  └── Miss
        │
        ▼
     Database
        │
        ▼
      Cache
        │
        ▼
      Return
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a read-through-style flow, the application retrieves data from the cache first. When the requested data is missing, it reads from the database and populates the cache.&lt;/p&gt;

&lt;p&gt;Strictly speaking, this is often implemented as a cache-aside pattern: the application explicitly manages cache reads, database reads, and cache population.&lt;/p&gt;

&lt;p&gt;For example, a NestJS service might use Redis to cache frequently accessed organizational settings, product catalog entries, or permission metadata.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrganizationSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OrganizationSettings&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`organization:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:settings`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;OrganizationSettings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;organizationRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;EX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example uses a five-minute expiration.&lt;/p&gt;

&lt;p&gt;The actual implementation should also validate cached data, handle serialization failures appropriately, and decide what happens when Redis is unavailable.&lt;/p&gt;

&lt;p&gt;The cache should improve the application's performance without becoming an accidental prerequisite for retrieving data that remains available in the primary database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache invalidation is the difficult part
&lt;/h2&gt;

&lt;p&gt;Adding a cache is easy.&lt;/p&gt;

&lt;p&gt;Keeping cached data correct is harder.&lt;/p&gt;

&lt;p&gt;Questions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When does the cache expire?&lt;/li&gt;
&lt;li&gt;When is it invalidated?&lt;/li&gt;
&lt;li&gt;What happens after an update?&lt;/li&gt;
&lt;li&gt;What happens if the database succeeds but cache invalidation fails?&lt;/li&gt;
&lt;li&gt;Can stale data be tolerated?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are architectural questions.&lt;/p&gt;

&lt;p&gt;Consider an organization whose approval limit changes from ₦500,000 to ₦1,000,000.&lt;/p&gt;

&lt;p&gt;If the database is updated but Redis still contains the previous limit, the application may continue making decisions based on outdated information.&lt;/p&gt;

&lt;p&gt;For display-oriented data, a short period of staleness may be acceptable. For authorization, financial controls, or approval decisions, it may not be.&lt;/p&gt;

&lt;p&gt;Common strategies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TTL-based expiration:&lt;/strong&gt; Allow entries to expire after a defined period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit invalidation:&lt;/strong&gt; Delete or update affected cache entries after a successful database write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned cache keys:&lt;/strong&gt; Include a version in the key to prevent old entries from being reused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-through approaches:&lt;/strong&gt; Update the database and cache through a coordinated application strategy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event-driven invalidation:&lt;/strong&gt; Publish changes and invalidate related entries through a reliable event-processing mechanism.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these strategies eliminates every failure mode.&lt;/p&gt;

&lt;p&gt;For example, if a database transaction commits successfully but the application crashes before invalidating Redis, the cache can remain stale.&lt;/p&gt;

&lt;p&gt;For important workflows, an outbox pattern can help: record the business change and an event in the same database transaction, then process that event asynchronously to invalidate or refresh the cache.&lt;/p&gt;

&lt;p&gt;The key lesson is that cache consistency must be designed, not assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis data structures are part of the value
&lt;/h2&gt;

&lt;p&gt;Redis is not limited to storing serialized JSON strings.&lt;/p&gt;

&lt;p&gt;It supports data structures with operations suited to different access patterns.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data structure&lt;/th&gt;
&lt;th&gt;Useful applications&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strings&lt;/td&gt;
&lt;td&gt;Cache entries, counters, temporary tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hashes&lt;/td&gt;
&lt;td&gt;Compact representations of objects and fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sets&lt;/td&gt;
&lt;td&gt;Unique membership, feature assignments, membership checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sorted sets&lt;/td&gt;
&lt;td&gt;Leaderboards, priority ordering, scheduled work indexes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lists&lt;/td&gt;
&lt;td&gt;Simple ordered collections and queue-like operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streams&lt;/td&gt;
&lt;td&gt;Append-only event records and consumer groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitmaps&lt;/td&gt;
&lt;td&gt;Compact tracking of binary states&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HyperLogLogs&lt;/td&gt;
&lt;td&gt;Approximate distinct-count analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important distinction is between choosing Redis because it is available and choosing a Redis data structure because its operations match the problem.&lt;/p&gt;

&lt;p&gt;For example, a sorted set can support a leaderboard without repeatedly sorting an entire dataset in application memory.&lt;/p&gt;

&lt;p&gt;A set can efficiently track membership, such as which feature flags have been assigned to a particular group.&lt;/p&gt;

&lt;p&gt;A counter can support lightweight operational metrics, subject to appropriate expiration, cardinality, and retention policies.&lt;/p&gt;

&lt;p&gt;The data structure should follow the access pattern and consistency requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limiting
&lt;/h2&gt;

&lt;p&gt;Rate limiting is another common use of Redis.&lt;/p&gt;

&lt;p&gt;An API may need to restrict how frequently a client can make requests within a given period.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Incoming Request
      │
      ▼
Identify Client
      │
      ▼
Check Rate Limit in Redis
      │
      ├── Within Limit → Continue
      │
      └── Limit Exceeded → Reject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Redis-backed limiter can maintain counters shared across multiple application instances.&lt;/p&gt;

&lt;p&gt;Without shared state, an application running on four replicas could enforce four independent limits instead of one limit across the entire deployment.&lt;/p&gt;

&lt;p&gt;Redis operations such as &lt;code&gt;INCR&lt;/code&gt; and expiration can implement simple fixed-window limits. However, the counter update and expiration setup must be designed to avoid race conditions, and a fixed window can allow bursts around window boundaries.&lt;/p&gt;

&lt;p&gt;Depending on the requirements, a sliding-window or token-bucket algorithm may be more appropriate.&lt;/p&gt;

&lt;p&gt;Rate limiting is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login and password-reset endpoints.&lt;/li&gt;
&lt;li&gt;Public APIs.&lt;/li&gt;
&lt;li&gt;OTP and verification-code requests.&lt;/li&gt;
&lt;li&gt;Expensive report-generation endpoints.&lt;/li&gt;
&lt;li&gt;Protection against accidental request bursts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key design questions include the limiting identity, the time window, the permitted burst size, and the behavior when Redis becomes unavailable.&lt;/p&gt;

&lt;p&gt;A global rate limit, a per-user limit, and a per-IP limit are different policies. They should not be treated as interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session management and temporary state
&lt;/h2&gt;

&lt;p&gt;Redis can also store temporary application state.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server-side sessions.&lt;/li&gt;
&lt;li&gt;Short-lived verification challenges.&lt;/li&gt;
&lt;li&gt;Temporary workflow state.&lt;/li&gt;
&lt;li&gt;One-time application tokens.&lt;/li&gt;
&lt;li&gt;Expiring reservations or locks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is particularly useful when an application runs across multiple instances.&lt;/p&gt;

&lt;p&gt;If a session exists only in the memory of one Node.js process, a subsequent request routed to another instance may not find it.&lt;/p&gt;

&lt;p&gt;Shared session storage allows instances to retrieve the same session state.&lt;/p&gt;

&lt;p&gt;However, not every token should automatically be stored in Redis. The application must define expiration, revocation, access controls, and the consequences of Redis data loss.&lt;/p&gt;

&lt;p&gt;For example, a password-reset challenge should expire and should not remain valid indefinitely simply because its key was never explicitly deleted.&lt;/p&gt;

&lt;p&gt;Temporary state is still security-relevant state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distributed locks and coordination
&lt;/h2&gt;

&lt;p&gt;Suppose several application instances attempt to generate the same monthly report simultaneously.&lt;/p&gt;

&lt;p&gt;Or imagine multiple workers trying to perform the same scheduled reconciliation.&lt;/p&gt;

&lt;p&gt;Redis can help coordinate such work through carefully designed locking or deduplication mechanisms.&lt;/p&gt;

&lt;p&gt;A simplified lock acquisition might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SET lock:monthly-report token NX PX 30000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NX&lt;/code&gt; requests creation only if the key does not already exist.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PX 30000&lt;/code&gt; gives the lock a 30-second expiration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;token&lt;/code&gt; should be a unique ownership value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The owner must release the lock only if the stored token still matches its own token. Otherwise, an expired lock could be acquired by another worker, and the original worker could accidentally delete the new owner's lock.&lt;/p&gt;

&lt;p&gt;A simple lock implementation is not automatically a complete distributed-locking solution.&lt;/p&gt;

&lt;p&gt;Long-running work, process pauses, network partitions, lock expiry, and delayed operations can all create concurrency problems.&lt;/p&gt;

&lt;p&gt;For operations where duplicate execution could cause serious damage, consider database constraints, idempotency keys, transactional guarantees, or fencing tokens in addition to coordination.&lt;/p&gt;

&lt;p&gt;A lock can reduce contention, but it should not replace the underlying correctness guarantees of the business operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background jobs with BullMQ
&lt;/h2&gt;

&lt;p&gt;Redis can also support queue infrastructure such as BullMQ.&lt;/p&gt;

&lt;p&gt;That creates a useful separation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    │
    ├── Cache
    │
    └── Job Queue
           │
           ▼
        Workers
           │
           ├── Email delivery
           ├── Report generation
           ├── File processing
           └── Notifications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the application should not confuse those responsibilities.&lt;/p&gt;

&lt;p&gt;Imagine an API endpoint that accepts a request to generate a large report.&lt;/p&gt;

&lt;p&gt;Generating the report inside the HTTP request can increase response times and consume application resources while the client waits.&lt;/p&gt;

&lt;p&gt;Instead, the API can validate the request, persist the necessary business state, enqueue a job, and return a job identifier.&lt;/p&gt;

&lt;p&gt;A worker processes the job independently.&lt;/p&gt;

&lt;p&gt;BullMQ supports capabilities such as delayed jobs, retries, repeatable scheduling, concurrency controls, and job prioritization.&lt;/p&gt;

&lt;p&gt;These capabilities are useful, but they do not mean every queued job will execute exactly once.&lt;/p&gt;

&lt;p&gt;A worker may complete an external side effect and crash before acknowledging the job. The queue may then retry it.&lt;/p&gt;

&lt;p&gt;Consequently, a production job handler should be designed to tolerate repeated execution.&lt;/p&gt;

&lt;p&gt;For example, an invoice-notification job should check whether the relevant notification has already been recorded or use an appropriate idempotency mechanism before performing a non-repeatable side effect.&lt;/p&gt;

&lt;p&gt;Similarly, retry policies should distinguish transient failures from permanent failures. Repeatedly retrying an invalid request does not make it valid.&lt;/p&gt;

&lt;p&gt;The queue is responsible for managing work. The application is responsible for making that work safe to execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pub/Sub and Redis Streams
&lt;/h2&gt;

&lt;p&gt;Redis can also support communication between application components.&lt;/p&gt;

&lt;p&gt;However, Pub/Sub and Streams serve different purposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis Pub/Sub&lt;/strong&gt; allows publishers to send messages to subscribers. It is useful for transient notifications, but messages are not retained for subscribers that are disconnected when the message is published.&lt;/p&gt;

&lt;p&gt;For example, an application could publish a notification that a dashboard should refresh its data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redis Streams&lt;/strong&gt; provide a retained, ordered stream of entries, with consumer-group capabilities for distributing processing across consumers and tracking pending messages.&lt;/p&gt;

&lt;p&gt;Streams can be useful for event-processing workflows that require consumers to resume work after interruptions.&lt;/p&gt;

&lt;p&gt;Even then, retention, acknowledgment, pending-message recovery, consumer coordination, and duplicate handling must be designed explicitly.&lt;/p&gt;

&lt;p&gt;Neither Pub/Sub nor Streams should automatically be treated as a substitute for a durable business record in a relational database.&lt;/p&gt;

&lt;p&gt;The appropriate choice depends on whether the application needs transient notification, retained event processing, or a durable source of business truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis is not automatically the source of truth
&lt;/h2&gt;

&lt;p&gt;One of the most important architectural decisions is determining which system owns the authoritative version of a piece of data.&lt;/p&gt;

&lt;p&gt;For many business applications, PostgreSQL or another primary database remains responsible for durable business records, constraints, and transactions.&lt;/p&gt;

&lt;p&gt;Redis can then support performance, coordination, temporary state, and asynchronous processing.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL
    │
    ├── Authoritative business records
    ├── Transactional constraints
    └── Durable workflow state

Redis
    │
    ├── Cached reads
    ├── Rate-limit counters
    ├── Temporary state
    └── Queue and coordination infrastructure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an architectural choice, not a universal rule that Redis can never be a primary datastore.&lt;/p&gt;

&lt;p&gt;Redis supports persistence and other durability-related features. However, its actual recovery guarantees depend on the configuration, persistence mode, replication, failover behavior, and operational setup.&lt;/p&gt;

&lt;p&gt;If losing a piece of data would violate a business requirement, its storage and recovery strategy must be evaluated accordingly.&lt;/p&gt;

&lt;p&gt;A cache entry can usually be rebuilt.&lt;/p&gt;

&lt;p&gt;An unrecorded payment, an unprocessed business obligation, or a lost approval decision may have much more serious consequences.&lt;/p&gt;

&lt;p&gt;Those data categories should not receive the same reliability assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redis introduces operational trade-offs
&lt;/h2&gt;

&lt;p&gt;Redis can improve application performance, but it also introduces infrastructure and operational costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory management
&lt;/h3&gt;

&lt;p&gt;Redis is commonly used as an in-memory datastore. Memory is therefore a resource that must be monitored and managed.&lt;/p&gt;

&lt;p&gt;Applications should consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory limits and eviction policies.&lt;/li&gt;
&lt;li&gt;Key expiration and retention.&lt;/li&gt;
&lt;li&gt;Large values and expensive data structures.&lt;/li&gt;
&lt;li&gt;Unexpected growth in the number of keys.&lt;/li&gt;
&lt;li&gt;Cache churn and eviction rates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An eviction policy that works well for a disposable cache may be inappropriate for data that the application expects to retain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection management
&lt;/h3&gt;

&lt;p&gt;Creating a new Redis connection for every request is usually a poor approach.&lt;/p&gt;

&lt;p&gt;Node.js applications should generally reuse managed connections, configure sensible timeouts, and handle reconnection behavior deliberately.&lt;/p&gt;

&lt;p&gt;Connection pooling or multiplexing should follow the behavior of the selected Redis client and workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis outages
&lt;/h3&gt;

&lt;p&gt;A cache outage and a queue outage have different consequences.&lt;/p&gt;

&lt;p&gt;If Redis is used only as a cache, the application might temporarily fall back to the database, provided that doing so will not overwhelm it.&lt;/p&gt;

&lt;p&gt;If Redis also backs background-job infrastructure, an outage may prevent jobs from being enqueued or processed.&lt;/p&gt;

&lt;p&gt;A fallback strategy must account for these differences.&lt;/p&gt;

&lt;p&gt;For example, sending every cache miss directly to PostgreSQL during a Redis outage can create a database traffic spike. A controlled fallback, load shedding, or temporary rejection of nonessential requests may be safer.&lt;/p&gt;

&lt;p&gt;Likewise, silently discarding a job because Redis is unavailable may violate the business contract of the endpoint.&lt;/p&gt;

&lt;p&gt;Resilience begins with identifying which operations can fail open, which must fail closed, and which require durable recovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring
&lt;/h3&gt;

&lt;p&gt;A production deployment should monitor Redis as an infrastructure dependency.&lt;/p&gt;

&lt;p&gt;Useful signals include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory consumption and evictions.&lt;/li&gt;
&lt;li&gt;Command latency and slow operations.&lt;/li&gt;
&lt;li&gt;Connection counts and rejected connections.&lt;/li&gt;
&lt;li&gt;Cache hit and miss rates.&lt;/li&gt;
&lt;li&gt;Queue depth and oldest-job age.&lt;/li&gt;
&lt;li&gt;Failed, stalled, and repeatedly retried jobs.&lt;/li&gt;
&lt;li&gt;Replication health and persistence-related failures where applicable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics help distinguish an application problem from an infrastructure problem.&lt;/p&gt;

&lt;p&gt;A cache hit rate alone is not sufficient evidence that Redis is improving the system. The cache must also reduce meaningful latency or database load without introducing unacceptable staleness or operational complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical rule for backend engineers
&lt;/h2&gt;

&lt;p&gt;Before introducing Redis into a feature, answer these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What problem are we solving?&lt;/strong&gt; Is it repeated computation, request throttling, coordination, temporary state, or deferred work?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is the source of truth?&lt;/strong&gt; Can the data be reconstructed, or must it survive failure?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What consistency guarantees are required?&lt;/strong&gt; Can the application tolerate stale data or duplicate processing?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens when Redis is unavailable?&lt;/strong&gt; Can the operation safely fall back, wait, retry, or fail?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How will the feature be monitored?&lt;/strong&gt; Which metrics will show that the design is working?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is Redis necessary?&lt;/strong&gt; Would a database constraint, an existing queue, or a simpler application-level solution solve the problem with fewer moving parts?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not to use Redis in as many places as possible.&lt;/p&gt;

&lt;p&gt;It is to use it where its data structures and operational characteristics provide a clear advantage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Redis is a useful infrastructure component.&lt;/p&gt;

&lt;p&gt;The important engineering skill is not knowing that Redis exists.&lt;/p&gt;

&lt;p&gt;It is understanding &lt;strong&gt;which problem Redis is solving in a particular part of the architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A cache should have an explicit consistency strategy. A queue should have a retry and idempotency strategy. A rate limiter should have a defined policy. A distributed lock should have clear ownership and failure semantics.&lt;/p&gt;

&lt;p&gt;Those decisions are what separate simply adding Redis to a backend from designing a backend that uses Redis responsibly.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>node</category>
      <category>nestjs</category>
      <category>backenddevelopment</category>
    </item>
    <item>
      <title>Why Choose RESTApi for Notifications?</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/topzyray/why-i-chose-rest-for-notifications-3c24</link>
      <guid>https://dev.to/topzyray/why-i-chose-rest-for-notifications-3c24</guid>
      <description>&lt;p&gt;&lt;em&gt;Choose REST, polling, SSE, or WebSockets for notifications based on delivery needs, while keeping notification processing reliable and asynchronous.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Not every notification system needs WebSockets.&lt;/p&gt;

&lt;p&gt;That is one of the architectural decisions I have been thinking through while designing backend communication systems.&lt;/p&gt;

&lt;p&gt;Real-time communication has its place, but introducing it by default can add complexity without necessarily improving the user experience. The better approach is to understand the requirements first and select the communication model that satisfies them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the requirement
&lt;/h2&gt;

&lt;p&gt;Before selecting WebSockets, Server-Sent Events (SSE), or polling, we should ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does the client actually need?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the requirement is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrieve notifications.&lt;/li&gt;
&lt;li&gt;Mark notifications as read or unread.&lt;/li&gt;
&lt;li&gt;Manage notification preferences.&lt;/li&gt;
&lt;li&gt;Retrieve notification data when the client requests it.&lt;/li&gt;
&lt;li&gt;View notifications when opening a page or refreshing a list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;REST can be sufficient.&lt;/p&gt;

&lt;p&gt;An HTTP-based API provides a straightforward contract for retrieving notifications, updating their read status, and managing preferences. It also fits naturally into applications that already use REST for their other backend interactions.&lt;/p&gt;

&lt;p&gt;The important point is that retrieving a notification and receiving it immediately are different requirements.&lt;/p&gt;

&lt;p&gt;If users only need to see their notifications when they open a page, refresh a list, or make a scheduled request, persistent connections may not be necessary.&lt;/p&gt;

&lt;p&gt;However, if the application must deliver new notifications to an already-open client with minimal delay, polling alone may not be enough. That requirement should influence the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why avoid unnecessary real-time complexity?
&lt;/h2&gt;

&lt;p&gt;Real-time communication introduces additional concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Persistent connections and their lifecycle.&lt;/li&gt;
&lt;li&gt;Reconnection and recovery after network interruptions.&lt;/li&gt;
&lt;li&gt;Connection management across multiple application instances.&lt;/li&gt;
&lt;li&gt;Load balancing and connection distribution.&lt;/li&gt;
&lt;li&gt;Connection state and resource consumption.&lt;/li&gt;
&lt;li&gt;Authentication and authorization throughout the connection lifecycle.&lt;/li&gt;
&lt;li&gt;Observability for connection failures and delivery delays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These problems can be justified when the product requires real-time updates.&lt;/p&gt;

&lt;p&gt;But they should be driven by requirements, not by the assumption that a more sophisticated communication mechanism is automatically a better one.&lt;/p&gt;

&lt;p&gt;For example, WebSockets may be appropriate for collaborative applications, live operational dashboards, chat systems, or workflows where clients need frequent bidirectional communication.&lt;/p&gt;

&lt;p&gt;For a conventional notification centre, however, an HTTP endpoint may provide everything the client needs.&lt;/p&gt;

&lt;p&gt;A simpler communication model can reduce operational overhead, make the API easier to test, and allow the team to focus on notification correctness rather than connection management.&lt;/p&gt;

&lt;p&gt;That does not mean REST is always cheaper or better. It means the additional complexity of real-time infrastructure should deliver a measurable benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notifications can be asynchronous internally
&lt;/h2&gt;

&lt;p&gt;Choosing REST for the client-facing API does not mean notification generation has to be synchronous.&lt;/p&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;p&gt;A business operation should not necessarily have to wait for an email, SMS, push notification, or in-app notification to be generated before it can complete.&lt;/p&gt;

&lt;p&gt;The architecture can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Business Event
      │
      ▼
Message / Job
      │
      ▼
Notification Worker
      │
      ▼
Notification Store
      │
      ▼
REST API
      │
      ▼
Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, when a requisition is approved, the business operation can record the approval and initiate the notification workflow. A background worker can then process the notification and persist the resulting in-app notification.&lt;/p&gt;

&lt;p&gt;The client retrieves that notification through REST.&lt;/p&gt;

&lt;p&gt;This separates three concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Business processing:&lt;/strong&gt; The operation that causes the notification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification processing:&lt;/strong&gt; The creation and handling of the notification, potentially through a background job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification retrieval:&lt;/strong&gt; The API through which the client accesses the notification.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each concern can evolve independently, provided the boundaries and contracts remain clear.&lt;/p&gt;

&lt;p&gt;There is also an important reliability consideration: if the business transaction commits but the notification job is never published, the user may not receive the expected notification. For workflows where this matters, a transactional outbox can help ensure that the intent to publish a notification is recorded atomically with the business change.&lt;/p&gt;

&lt;p&gt;Background processing also requires appropriate retry policies, idempotency, and failure monitoring. A job should not create duplicate notifications every time a worker retries an operation.&lt;/p&gt;

&lt;p&gt;REST simplifies the client-facing communication boundary; it does not remove the need for reliable asynchronous processing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preferences belong in the design
&lt;/h2&gt;

&lt;p&gt;A notification system also needs preferences.&lt;/p&gt;

&lt;p&gt;Different users or identities may have different preferences for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email.&lt;/li&gt;
&lt;li&gt;SMS.&lt;/li&gt;
&lt;li&gt;Push notifications.&lt;/li&gt;
&lt;li&gt;In-app notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those decisions should be represented explicitly rather than hard-coded into business workflows.&lt;/p&gt;

&lt;p&gt;For example, a business service should not need to know whether a particular user wants email notifications or push notifications. It should communicate the relevant business event or notification intent to the notification system, which can apply the appropriate preferences and delivery rules.&lt;/p&gt;

&lt;p&gt;Preferences may also depend on notification category, urgency, organization policy, or whether a channel is available and supported.&lt;/p&gt;

&lt;p&gt;However, preferences should not be confused with mandatory operational messages. Security alerts, compliance notices, and other critical communications may require different rules from ordinary informational notifications.&lt;/p&gt;

&lt;p&gt;Separating these concerns makes the system easier to maintain and allows notification channels to evolve without forcing changes throughout the business domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if real-time delivery becomes necessary?
&lt;/h2&gt;

&lt;p&gt;Choosing REST today does not mean ruling out real-time delivery permanently.&lt;/p&gt;

&lt;p&gt;The system can begin with a REST API for notification retrieval and introduce polling, SSE, or WebSockets later if actual product requirements justify the additional complexity.&lt;/p&gt;

&lt;p&gt;Each option has different characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;REST with polling:&lt;/strong&gt; The client periodically requests updates. It is straightforward, but frequent polling can generate unnecessary requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Sent Events (SSE):&lt;/strong&gt; The server streams updates to the client over a persistent, one-way connection. It can suit notification feeds where the server needs to push updates and the client does not need bidirectional messaging over the same connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets:&lt;/strong&gt; The client and server maintain a bidirectional communication channel. This is useful when both sides need to exchange frequent real-time messages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regardless of the delivery mechanism, the persisted notification record should remain the source of truth. A real-time event can tell the client that new data is available, but the client should not have to rely on a transient connection to recover notifications it missed while offline.&lt;/p&gt;

&lt;p&gt;That separation also makes it easier to support multiple client types, including web and mobile applications, without tying notification history to a particular connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Technology selection should follow requirements.&lt;/p&gt;

&lt;p&gt;If real-time delivery is necessary, real-time infrastructure may be justified.&lt;/p&gt;

&lt;p&gt;If it is not, REST can provide a simpler and more maintainable communication boundary.&lt;/p&gt;

&lt;p&gt;The goal is not to choose the technology with the most capabilities. It is to choose the simplest architecture that satisfies the requirements reliably, while leaving room for the system to evolve when those requirements change.&lt;/p&gt;

</description>
      <category>backendengineering</category>
      <category>systemdesign</category>
      <category>node</category>
      <category>notifications</category>
    </item>
    <item>
      <title>What a Failed Import Taught Me About Long-Running Data Operations</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Sun, 18 Oct 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/topzyray/what-a-failed-import-taught-me-about-long-running-data-operations-228h</link>
      <guid>https://dev.to/topzyray/what-a-failed-import-taught-me-about-long-running-data-operations-228h</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Learn how batching, durable progress tracking, idempotency, and targeted retries make long-running data imports recoverable after failures&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;When seeding initial data into a database, large data imports can be deceptively simple.&lt;/p&gt;

&lt;p&gt;The first implementation often looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read records
   ↓
Insert records
   ↓
Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That model changes when an operation involves thousands of records and a network failure occurs.&lt;/p&gt;

&lt;p&gt;The challenge is no longer just inserting data. It is ensuring that the operation can recover safely when something interrupts its progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with treating an import as one operation
&lt;/h2&gt;

&lt;p&gt;If an import processes thousands of records inside one enormous transaction, a failure can make recovery difficult. The transaction may hold locks for too long, consume substantial database resources, or exceed configured timeouts.&lt;/p&gt;

&lt;p&gt;If it processes records individually, performance may suffer because of repeated database round trips and transaction overhead.&lt;/p&gt;

&lt;p&gt;A practical approach is batching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Records
 │
 ├── Batch 1
 ├── Batch 2
 ├── Batch 3
 ├── ...
 └── Batch N
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of treating the entire dataset as one indivisible operation, the system processes manageable groups of records.&lt;/p&gt;

&lt;p&gt;Each batch can be validated, written, and committed independently, depending on the consistency requirements of the import.&lt;/p&gt;

&lt;p&gt;Batch size matters. Smaller batches generally reduce transaction duration and the amount of work that must be retried, but they can increase the number of database round trips. Larger batches may improve throughput, but they can consume more memory and take longer to process.&lt;/p&gt;

&lt;p&gt;The right batch size depends on the database, record complexity, constraints, indexes, available resources, and acceptable recovery cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batching creates a recovery point
&lt;/h2&gt;

&lt;p&gt;Suppose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Batch 1  → committed
Batch 2  → committed
...
Batch 47 → committed
Batch 48 → failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should not necessarily restart from the beginning.&lt;/p&gt;

&lt;p&gt;If the earlier batches have been committed successfully, the system should be able to identify that progress and resume from the appropriate point.&lt;/p&gt;

&lt;p&gt;However, batching alone does not guarantee resumability.&lt;/p&gt;

&lt;p&gt;The application needs a reliable way to determine which records or batches have completed. A counter held only in application memory is insufficient because the process may restart and lose that information.&lt;/p&gt;

&lt;p&gt;Depending on the import, progress can be tracked through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A durable checkpoint recording the last successfully processed batch.&lt;/li&gt;
&lt;li&gt;An import-job record containing the current status and progress.&lt;/li&gt;
&lt;li&gt;Stable source identifiers that allow the application to reconcile existing records.&lt;/li&gt;
&lt;li&gt;A combination of checkpoints and database state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checkpoint must accurately reflect committed work. If the system records a batch as complete before its database transaction commits, a failure could cause records to be skipped during recovery.&lt;/p&gt;

&lt;p&gt;Conversely, if the database commits but the process crashes before updating the checkpoint, the batch may run again. That is why safe retries and idempotency remain important even when checkpoints are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem is resumability
&lt;/h2&gt;

&lt;p&gt;A long-running operation should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What has completed?&lt;/li&gt;
&lt;li&gt;What failed?&lt;/li&gt;
&lt;li&gt;Where did the failure occur?&lt;/li&gt;
&lt;li&gt;Can the operation safely resume?&lt;/li&gt;
&lt;li&gt;Are records idempotent?&lt;/li&gt;
&lt;li&gt;Can a partially completed batch be retried?&lt;/li&gt;
&lt;li&gt;Can the system distinguish a failed operation from one that is still running?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions are more important than simply making the initial import work.&lt;/p&gt;

&lt;p&gt;For a production import, I would also want to know the total number of records, how many have succeeded or failed, when the operation last made progress, and whether the failure is transient or requires intervention.&lt;/p&gt;

&lt;p&gt;That information makes the operation observable and gives an operator a way to understand its state without reconstructing everything from application logs.&lt;/p&gt;

&lt;p&gt;A useful design is to represent the import as a durable job with explicit states, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PENDING
   ↓
RUNNING
   ├──→ COMPLETED
   ├──→ FAILED
   └──→ PAUSED / RETRYABLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact states depend on the requirements. What matters is that the system can explain its current state and recover from interruption without relying on assumptions about what happened before the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network failure is normal
&lt;/h2&gt;

&lt;p&gt;During one of the data-import workflows I worked through, a network connectivity failure occurred after many records had already been committed.&lt;/p&gt;

&lt;p&gt;The useful lesson was not the specific error.&lt;/p&gt;

&lt;p&gt;It was the architecture of the recovery problem.&lt;/p&gt;

&lt;p&gt;A network failure does not necessarily tell us whether the database operation failed. In some situations, the client loses its connection after the database has committed a transaction but before the client receives confirmation.&lt;/p&gt;

&lt;p&gt;The application may therefore be uncertain about the outcome.&lt;/p&gt;

&lt;p&gt;Blindly repeating the operation can create duplicates or violate constraints. Assuming success can leave records missing.&lt;/p&gt;

&lt;p&gt;This is why recovery logic should rely on durable database state, stable record identities, and safe retry behavior rather than treating every connection error as proof that nothing was committed.&lt;/p&gt;

&lt;p&gt;A long-running operation must be designed with interruption in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency
&lt;/h2&gt;

&lt;p&gt;If we retry a batch, we should avoid creating duplicates or corrupting existing data.&lt;/p&gt;

&lt;p&gt;That means the import needs a stable identity for each record or another mechanism for safely determining whether the record has already been processed.&lt;/p&gt;

&lt;p&gt;For example, an imported personnel record might have a unique service number, while an organizational record might have a stable directory code. These identifiers can help the application reconcile incoming data with existing records.&lt;/p&gt;

&lt;p&gt;In PostgreSQL, unique constraints provide an important database-level safeguard. Depending on the intended behavior, the import can use an insert, an upsert, or an explicit reconciliation step.&lt;/p&gt;

&lt;p&gt;However, idempotency is not simply a matter of ignoring duplicate-key errors. The system must define what should happen when an existing record is encountered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should the existing record remain unchanged?&lt;/li&gt;
&lt;li&gt;Should selected fields be updated?&lt;/li&gt;
&lt;li&gt;Should conflicting values cause the import to fail?&lt;/li&gt;
&lt;li&gt;Should the conflict be recorded for manual review?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The answer depends on whether the import is intended to create new records, synchronize an external source, or update an existing dataset.&lt;/p&gt;

&lt;p&gt;The application and database should enforce the chosen behavior consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrying the right unit of work
&lt;/h2&gt;

&lt;p&gt;Another important consideration is deciding what to retry.&lt;/p&gt;

&lt;p&gt;If a batch fails, retrying the entire import may waste time and repeat completed work. Retrying only the failed batch is often more efficient, provided the batch can be executed safely more than once.&lt;/p&gt;

&lt;p&gt;A practical retry strategy should distinguish transient failures, such as temporary network interruptions or database unavailability, from permanent failures, such as invalid data or violated business rules.&lt;/p&gt;

&lt;p&gt;Transient failures may justify a limited number of retries with exponential backoff and jitter. Permanent failures usually require correction rather than repeated execution.&lt;/p&gt;

&lt;p&gt;For partially processed batches, the design must also establish the unit of atomicity. If each batch is committed as one transaction, a failed transaction should roll back its database changes. If records are committed individually, the retry mechanism must account for the records that succeeded before the failure.&lt;/p&gt;

&lt;p&gt;This distinction affects how checkpoints are recorded and how much work recovery may need to repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;A production data import is not just an insertion script.&lt;/p&gt;

&lt;p&gt;It is a recoverable workflow.&lt;/p&gt;

&lt;p&gt;Batching helps control the amount of work performed at once. Durable progress tracking makes recovery possible. Idempotency makes retries safer. Observability makes failures understandable.&lt;/p&gt;

&lt;p&gt;Once operations become long-running, failure and resumption need to be considered part of the design rather than exceptional afterthoughts.&lt;/p&gt;

</description>
      <category>backendengineering</category>
      <category>reliability</category>
      <category>database</category>
      <category>node</category>
    </item>
    <item>
      <title>Designing Audit Trails for Systems That Matter</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Thu, 15 Oct 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/topzyray/designing-audit-trails-for-systems-that-matter-512a</link>
      <guid>https://dev.to/topzyray/designing-audit-trails-for-systems-that-matter-512a</guid>
      <description>&lt;p&gt;Design audit trails that record who did what, when, and to which resource, supporting accountability in business-critical systems.&lt;/p&gt;

&lt;p&gt;An audit log is not simply a table containing timestamps.&lt;/p&gt;

&lt;p&gt;In systems involving inventory, approvals, assets and organizational responsibilities, knowing that something happened is not enough.&lt;/p&gt;

&lt;p&gt;We also need to know what happened and who initiated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auditability changes how a system is designed
&lt;/h2&gt;

&lt;p&gt;Consider a requisition.&lt;/p&gt;

&lt;p&gt;A useful audit trail may need to answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Who?
What?
When?
Which resource?
Which action?
What changed?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fundamentally different from simply logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request completed successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Audit and application logs are different
&lt;/h2&gt;

&lt;p&gt;Application logs help us as engineers understand system behavior.&lt;/p&gt;

&lt;p&gt;Audit records serve a different purpose.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application log:
Database connection failed.

Audit record:
User approved requisition X.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first helps operate the system.&lt;/p&gt;

&lt;p&gt;The second records a business action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit sensitive workflows
&lt;/h2&gt;

&lt;p&gt;The NIS Store MIS contains workflows involving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;requisitions;&lt;/li&gt;
&lt;li&gt;approvals;&lt;/li&gt;
&lt;li&gt;inventory;&lt;/li&gt;
&lt;li&gt;assets;&lt;/li&gt;
&lt;li&gt;dispatch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are precisely the kinds of operations where traceability becomes important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not record meaningless noise
&lt;/h2&gt;

&lt;p&gt;Auditing everything can produce an enormous amount of information without creating useful accountability.&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which business actions must remain traceable?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creation;&lt;/li&gt;
&lt;li&gt;approval;&lt;/li&gt;
&lt;li&gt;rejection;&lt;/li&gt;
&lt;li&gt;dispatch;&lt;/li&gt;
&lt;li&gt;assignment;&lt;/li&gt;
&lt;li&gt;status changes;&lt;/li&gt;
&lt;li&gt;sensitive administrative changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Audit records should be difficult to misinterpret
&lt;/h2&gt;

&lt;p&gt;An audit record should provide enough context to understand the action without requiring the reader to reconstruct the entire request from unrelated application logs.&lt;/p&gt;

&lt;p&gt;This is especially important when an organization needs to investigate what happened later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate audits records before persistence
&lt;/h2&gt;

&lt;p&gt;It is germane to mention this because paying attention to details ensure systems are safer from attackers.&lt;/p&gt;

&lt;p&gt;I mean Audit Input Validation (AIV).&lt;/p&gt;

&lt;p&gt;Validating audits input before persisting in the database can help reduce attack vectors on core infrastructure especially the DATABASE.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Auditability is not something that should be bolted onto an enterprise system at the end.&lt;/p&gt;

&lt;p&gt;It should be considered when designing important business workflows.&lt;/p&gt;

&lt;p&gt;If an operation matters to the organization, the system should make its history understandable.&lt;/p&gt;

</description>
      <category>backendengineering</category>
      <category>enterprisesystems</category>
      <category>audits</category>
      <category>security</category>
    </item>
  </channel>
</rss>
