<?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>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>
    <item>
      <title>RBAC Is Not Enough: Thinking About Authorization in Enterprise Systems</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Tue, 13 Oct 2026 06:21:18 +0000</pubDate>
      <link>https://dev.to/topzyray/rbac-is-not-enough-thinking-about-authorization-in-enterprise-systems-4m2d</link>
      <guid>https://dev.to/topzyray/rbac-is-not-enough-thinking-about-authorization-in-enterprise-systems-4m2d</guid>
      <description>&lt;p&gt;Roles are one of the first authorization mechanisms most backend developers encounter.&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;admin
manager
user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server checks the user's role and allows or rejects an operation.&lt;/p&gt;

&lt;p&gt;That is useful.&lt;/p&gt;

&lt;p&gt;But enterprise systems quickly demonstrate that roles alone are often insufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The NIS Store MIS example
&lt;/h2&gt;

&lt;p&gt;The system I have been working on above includes roles representing different responsibilities.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;system administrator;&lt;/li&gt;
&lt;li&gt;operational administrator;&lt;/li&gt;
&lt;li&gt;store administrator;&lt;/li&gt;
&lt;li&gt;multi-layered approving authority&lt;/li&gt;
&lt;li&gt;store manager;&lt;/li&gt;
&lt;li&gt;store officer;&lt;/li&gt;
&lt;li&gt;department officer;&lt;/li&gt;
&lt;li&gt;request officer;&lt;/li&gt;
&lt;li&gt;dispatch officer;&lt;/li&gt;
&lt;li&gt;auditor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important observation is that a person's role does not necessarily tell us everything about what they can access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Role versus context
&lt;/h2&gt;

&lt;p&gt;Consider two users with the same role.&lt;/p&gt;

&lt;p&gt;They may still belong to different offices.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Same role
+
Different office
=
Potentially different access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where authorization becomes contextual.&lt;/p&gt;

&lt;h2&gt;
  
  
  RBAC
&lt;/h2&gt;

&lt;p&gt;Role-Based Access Control answers questions such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this user have the &lt;code&gt;store_manager&lt;/code&gt; role?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is useful for coarse-grained 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 plaintext"&gt;&lt;code&gt;store_manager → approve requisition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the system may also need to ask (which it actually does):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this requisition within the organizational scope this user is allowed to manage? (since it is a multi-tenant system)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is no longer just role checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authorization should be explicit
&lt;/h2&gt;

&lt;p&gt;A useful authorization decision can be thought of as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Identity
   +
Role
   +
Resource
   +
Action
   +
Context
   =
Authorization Decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us a more realistic model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid scattering authorization
&lt;/h2&gt;

&lt;p&gt;One of the problems with poorly designed systems is authorization logic appearing everywhere:&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;if &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;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside controllers, services and repositories.&lt;/p&gt;

&lt;p&gt;That becomes difficult to audit.&lt;/p&gt;

&lt;p&gt;Authorization should instead have recognizable policy boundaries.&lt;/p&gt;

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

&lt;p&gt;RBAC remains valuable.&lt;/p&gt;

&lt;p&gt;The lesson is not to abandon it.&lt;/p&gt;

&lt;p&gt;The lesson is to recognize when the system has moved beyond simple role-based decisions.&lt;/p&gt;

&lt;p&gt;Enterprise authorization often needs identity, role, resource and organizational context to make a correct decision.&lt;/p&gt;

</description>
      <category>node</category>
      <category>security</category>
      <category>nestjs</category>
      <category>backend</category>
    </item>
    <item>
      <title>Sessions, Devices and JWTs in a Multi-Client Application</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Mon, 12 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/topzyray/sessions-devices-and-jwts-in-a-multi-client-application-3ch7</link>
      <guid>https://dev.to/topzyray/sessions-devices-and-jwts-in-a-multi-client-application-3ch7</guid>
      <description>&lt;p&gt;Design production session management for JWT applications, including device tracking, session revocation, and web and mobile clients could be tricky.&lt;/p&gt;

&lt;p&gt;JWT authentication is easy to demonstrate.&lt;/p&gt;

&lt;p&gt;Production session management is harder.&lt;/p&gt;

&lt;p&gt;A token can tell the server who a user is, but a production authentication system also needs to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which device created the session?&lt;/li&gt;
&lt;li&gt;How many active sessions should a user have?&lt;/li&gt;
&lt;li&gt;Can one session be revoked?&lt;/li&gt;
&lt;li&gt;What happens after logout?&lt;/li&gt;
&lt;li&gt;How does a mobile application behave differently from a browser?&lt;/li&gt;
&lt;li&gt;How can suspicious sessions be identified?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are session-management questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat sessions as first-class data
&lt;/h2&gt;

&lt;p&gt;Instead of thinking only about a JWT, I think about:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;A session can carry information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identity;&lt;/li&gt;
&lt;li&gt;device identifier;&lt;/li&gt;
&lt;li&gt;creation time;&lt;/li&gt;
&lt;li&gt;last activity;&lt;/li&gt;
&lt;li&gt;expiration;&lt;/li&gt;
&lt;li&gt;revocation state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a much more manageable model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limiting sessions
&lt;/h2&gt;

&lt;p&gt;One design I have worked with uses a maximum number of active sessions per identity.&lt;/p&gt;

&lt;p&gt;The important principle is that session limits should be enforced deliberately rather than accidentally.&lt;/p&gt;

&lt;p&gt;When a new session is created, the application can determine whether existing sessions must be invalidated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Device identity
&lt;/h2&gt;

&lt;p&gt;For mobile applications, a device identifier can help distinguish sessions.&lt;/p&gt;

&lt;p&gt;This does not make the device automatically trustworthy.&lt;/p&gt;

&lt;p&gt;It simply gives the system another piece of information for session management.&lt;/p&gt;

&lt;p&gt;The backend can then reason about:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Revocation matters
&lt;/h2&gt;

&lt;p&gt;A purely stateless JWT design can make immediate revocation difficult.&lt;/p&gt;

&lt;p&gt;If the server issues a long-lived token and has no server-side session state, invalidating one token becomes harder.&lt;/p&gt;

&lt;p&gt;This is why session design often combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;short-lived access credentials;&lt;/li&gt;
&lt;li&gt;refresh/session state;&lt;/li&gt;
&lt;li&gt;server-side revocation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact implementation depends on the application's threat model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile authentication
&lt;/h2&gt;

&lt;p&gt;Mobile applications also introduce concerns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;secure local storage;&lt;/li&gt;
&lt;li&gt;biometric authentication;&lt;/li&gt;
&lt;li&gt;token refresh;&lt;/li&gt;
&lt;li&gt;device lifecycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the systems I have been designing, mobile authentication therefore cannot simply be treated as a browser authentication flow with a different UI.&lt;/p&gt;

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

&lt;p&gt;JWTs are a mechanism.&lt;/p&gt;

&lt;p&gt;Authentication architecture is a system.&lt;/p&gt;

&lt;p&gt;Once sessions, devices, revocation and multiple clients enter the picture, the design needs to go beyond simply generating a token.&lt;/p&gt;

</description>
      <category>backendengineering</category>
      <category>security</category>
    </item>
    <item>
      <title>Building Production Backends with NestJS</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Fri, 09 Oct 2026 10:05:09 +0000</pubDate>
      <link>https://dev.to/topzyray/building-production-backends-with-nestjs-4a1</link>
      <guid>https://dev.to/topzyray/building-production-backends-with-nestjs-4a1</guid>
      <description>&lt;p&gt;My focus for this month of October will be on NestJS framework and how it has imparted my development experience as a fullstack software engineer. NestJS provides a structured framework for building Node.js applications, but the framework alone does not determine whether an application will be maintainable in production.&lt;/p&gt;

&lt;p&gt;The architecture surrounding the framework matters a lot. I will be sharing overview on my opinions when developing a secure and scalable systems in NestJS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature-oriented modules
&lt;/h2&gt;

&lt;p&gt;I prefer organizing backend systems around business capabilities rather than technical file types.&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;src/
├── modules/
│   ├── users/
│   ├── authentication/
│   ├── payments/
│   └── notifications/
│
├── infrastructure/
├── shared/
└── main.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps related business functionality together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency boundaries
&lt;/h2&gt;

&lt;p&gt;Business logic should not directly depend on infrastructure details.&lt;/p&gt;

&lt;p&gt;For example, application code should depend on an abstraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;interface EmailSender &lt;span class="o"&gt;{&lt;/span&gt;
  send&lt;span class="o"&gt;(&lt;/span&gt;message: EmailMessage&lt;span class="o"&gt;)&lt;/span&gt;: Promise&amp;lt;void&amp;gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than directly depending on a specific provider.&lt;/p&gt;

&lt;p&gt;The infrastructure layer can then implement it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;class ResendEmailSender implements EmailSender &lt;span class="o"&gt;{&lt;/span&gt;
  async send&lt;span class="o"&gt;(&lt;/span&gt;message: EmailMessage&lt;span class="o"&gt;)&lt;/span&gt;: Promise&amp;lt;void&amp;gt; &lt;span class="o"&gt;{&lt;/span&gt;
    // provider implementation
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it possible to replace infrastructure without rewriting business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;The goal is not to create the largest possible architecture.&lt;/p&gt;

&lt;p&gt;The goal is to create a system where important decisions remain easy to change.&lt;/p&gt;

&lt;p&gt;That principle becomes increasingly important as an application grows.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>nestjs</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Feature-Based Architecture in Node.js</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/topzyray/feature-based-architecture-in-nodejs-28po</link>
      <guid>https://dev.to/topzyray/feature-based-architecture-in-nodejs-28po</guid>
      <description>&lt;p&gt;Compare feature-based and layer-based Node.js project structures, and see how organizing code by business capability supports growth.&lt;/p&gt;

&lt;p&gt;In my previous posts, I discussed why architecture should be more important than the framework and why infrastructure should not control business logic.&lt;/p&gt;

&lt;p&gt;The next question is practical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should the codebase itself be organized?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the systems I have been working on, I increasingly prefer organizing code around business capabilities rather than putting every controller, service, repository and model into global folders.&lt;/p&gt;

&lt;p&gt;This is what I mean by feature-based architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The traditional structure
&lt;/h2&gt;

&lt;p&gt;A common Node.js project starts 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;src/
├── controllers/
├── services/
├── repositories/
├── models/
├── validators/
└── utils/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for small applications.&lt;/p&gt;

&lt;p&gt;As the application grows, however, finding everything related to one business capability becomes harder.&lt;/p&gt;

&lt;p&gt;A user-related change may involve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;controllers/UserController.ts
services/UserService.ts
repositories/UserRepository.ts
models/User.ts
validators/UserValidator.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The files are separated by technical role rather than business responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organizing around features
&lt;/h2&gt;

&lt;p&gt;An alternative 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/
│   ├── users/
│   ├── posts/
│   ├── orders/
│   └── notifications/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each feature can contain its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;domain/
application/
infrastructure/
presentation/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is particularly useful for larger systems.&lt;/p&gt;

&lt;p&gt;In the NIS Store MIS work, for instance, the system has distinct business areas such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identity and access management;&lt;/li&gt;
&lt;li&gt;requisitions;&lt;/li&gt;
&lt;li&gt;approvals;&lt;/li&gt;
&lt;li&gt;assets;&lt;/li&gt;
&lt;li&gt;inventory;&lt;/li&gt;
&lt;li&gt;dispatch;&lt;/li&gt;
&lt;li&gt;organizations;&lt;/li&gt;
&lt;li&gt;notifications;&lt;/li&gt;
&lt;li&gt;auditing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not merely technical categories.&lt;/p&gt;

&lt;p&gt;They represent different areas of the business.&lt;/p&gt;

&lt;h2&gt;
  
  
  A feature is a boundary
&lt;/h2&gt;

&lt;p&gt;A useful feature boundary answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What business capability does this module own?&lt;/p&gt;
&lt;/blockquote&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;requisitions/
├── domain/
├── application/
├── infrastructure/
└── presentation/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The requisition module should own requisition-specific behavior.&lt;/p&gt;

&lt;p&gt;The approval module should own approval-specific behavior.&lt;/p&gt;

&lt;p&gt;This makes the architecture easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature-based does not mean abandoning layers
&lt;/h2&gt;

&lt;p&gt;Feature-based architecture and layered architecture are not opposites.&lt;/p&gt;

&lt;p&gt;They can work together.&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;modules/
└── requisitions/
    ├── domain/
    ├── application/
    ├── infrastructure/
    └── presentation/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feature provides the boundary.&lt;/p&gt;

&lt;p&gt;The internal layers provide separation of responsibilities.&lt;/p&gt;

&lt;p&gt;This combination is more useful to me than choosing one pattern dogmatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;As systems grow, organization becomes part of architecture.&lt;/p&gt;

&lt;p&gt;A codebase should make it obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where a business rule lives;&lt;/li&gt;
&lt;li&gt;which module owns it;&lt;/li&gt;
&lt;li&gt;which infrastructure implements it;&lt;/li&gt;
&lt;li&gt;where its HTTP interface is exposed;&lt;/li&gt;
&lt;li&gt;how it can be tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good folder structure cannot fix bad architecture.&lt;/p&gt;

&lt;p&gt;But good boundaries make bad coupling easier to detect.&lt;/p&gt;

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

&lt;p&gt;The purpose of feature-based architecture is not to make the folder tree look sophisticated.&lt;/p&gt;

&lt;p&gt;It is to make the business structure visible in the code.&lt;/p&gt;

&lt;p&gt;When I open a production backend, I want to understand the system by understanding its capabilities.&lt;/p&gt;

&lt;p&gt;That is the direction I am increasingly taking with my Node.js architecture.&lt;/p&gt;

</description>
      <category>backendengineering</category>
      <category>architecture</category>
      <category>typescript</category>
      <category>node</category>
    </item>
    <item>
      <title>From Silence to Sound: Fixing Audio on Ubuntu Linux on my Samsung Galaxy Book</title>
      <dc:creator>Tope Taiwo</dc:creator>
      <pubDate>Sat, 02 Aug 2025 22:58:42 +0000</pubDate>
      <link>https://dev.to/topzyray/from-silence-to-sound-fixing-audio-on-samsung-galaxy-book-running-ubuntu-linux-4kgc</link>
      <guid>https://dev.to/topzyray/from-silence-to-sound-fixing-audio-on-samsung-galaxy-book-running-ubuntu-linux-4kgc</guid>
      <description>&lt;p&gt;Over four days. That’s how long I spent trying to get the internal speakers on my Samsung Galaxy Book2 (NT950XDA) to produce a single sound after switching from Windows to Ubuntu.&lt;/p&gt;

&lt;p&gt;And I wasn’t alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Right out of the box, Ubuntu (even the latest 24.04) doesn’t play well with the Realtek ALC298 audio codec on many Samsung Galaxy Book devices. Headphones might work, but internal speakers remain silent due to uninitialized smart speaker amps.&lt;/p&gt;

&lt;p&gt;I found myself crawling through obscure forum posts, GitHub issues, and deep dmesg logs. I came across broken workarounds, kernel parameters, even dead-end firmware downloads. Nothing stuck — until I discovered a promising thread from the Manjaro community and a early work of @&lt;a href="https://github.com/joshuagrisham/samsung-galaxybook-extras" rel="noopener noreferrer"&gt;joshuagrisham&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Breakthrough
&lt;/h2&gt;

&lt;p&gt;The answer turned out to be &lt;code&gt;hda-verb&lt;/code&gt;, a tool for sending manual codec commands to initialize the amp. After copying dozens of lines of hda-verb commands into a script and running it manually, audio finally burst through my speakers.&lt;/p&gt;

&lt;p&gt;It worked—but only until the next reboot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Automation
&lt;/h2&gt;

&lt;p&gt;That’s when I decided to build a real solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-run the fix at boot and after resume&lt;/li&gt;
&lt;li&gt;Avoid breaking any existing audio setups&lt;/li&gt;
&lt;li&gt;Use systemd services for reliability&lt;/li&gt;
&lt;li&gt;Keep it simple, safe, and open source&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Tool: galaxybook-linux-setup
&lt;/h2&gt;

&lt;p&gt;Now available on GitHub: &lt;a href="https://github.com/topzyray/galaxybook-linux-setup" rel="noopener noreferrer"&gt;https://github.com/topzyray/galaxybook-linux-setup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🛠 Initializes smart speaker amps via hda-verb&lt;/li&gt;
&lt;li&gt;🌀 Runs fix on boot + resume using systemd&lt;/li&gt;
&lt;li&gt;🎚 Disables PulseAudio's suspend-on-idle to prevent speaker freezing&lt;/li&gt;
&lt;li&gt;📁 Backs up existing config files to avoid damage&lt;/li&gt;
&lt;li&gt;⚙️ Detects your current user and system safely&lt;/li&gt;
&lt;li&gt;🤝 MIT-licensed, contributions welcome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Installation is as easy as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/topzyray/galaxybook-linux-setup.git
cd galaxybook-linux-setup
chmod +x install.sh
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reboot when prompted, and you’re done. No more silent boots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who This Might Helps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Galaxy Book2 NT950XDA users or other device with similar &lt;a href="https://github.com/joshuagrisham/samsung-galaxybook-extras/issues/80" rel="noopener noreferrer"&gt;issue or device/driver info&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Anyone with Realtek ALC298 codec and no speaker output&lt;/li&gt;
&lt;li&gt;Linux newcomers switching from Windows&lt;/li&gt;
&lt;li&gt;Ubuntu/Mint/Pop_OS/Manjaro users on 5.8+ kernels though I tested on 6.14.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Credit Where Due
&lt;/h2&gt;

&lt;p&gt;Joshua Grisham @joshuagrisham – &lt;a href="https://github.com/joshuagrisham/samsung-galaxybook-extras" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;
Community post from user @drlucas – &lt;a href="https://forum.manjaro.org/t/howto-set-up-the-audio-card-in-samsung-galaxy-book/37090" rel="noopener noreferrer"&gt;Manjaro Forum&lt;/a&gt;&lt;br&gt;
Access hda-ver used &lt;a href="https://pastebin.com/raw/zsXp2vz6" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;This project reminded me why open source matters. Without the knowledge of those before me, I’d still be rebooting into silence.&lt;/p&gt;

&lt;p&gt;Now, I’m extending this to include fingerprint readers, screen brightness fixes, and more for Galaxy Book users and I will also be testing on other devices. Please you can test too. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE: The script does not delete any file rather it create a backup whenever updating an existing file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Have a similar issue? Found this useful? Contribute, fork, share, or open an issue!&lt;/p&gt;

&lt;p&gt;👉 Repo: &lt;a href="https://github.com/topzyray/galaxybook-linux-setup" rel="noopener noreferrer"&gt;https://github.com/topzyray/galaxybook-linux-setup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have further question, suggestion or observations please leave them in the comment section. Thank you for reading.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ubuntu</category>
      <category>opensource</category>
      <category>samsung</category>
    </item>
  </channel>
</rss>
