<?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: Poojan</title>
    <description>The latest articles on DEV Community by Poojan (@poojang).</description>
    <link>https://dev.to/poojang</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%2F3986930%2Fcc62937c-0f36-4ae3-bc93-44c620e36da9.png</url>
      <title>DEV Community: Poojan</title>
      <link>https://dev.to/poojang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/poojang"/>
    <language>en</language>
    <item>
      <title>I Have Idempotent Search Endpoints Doing POST. Should They Be QUERY?</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/i-have-idempotent-search-endpoints-doing-post-should-they-be-query-305a</link>
      <guid>https://dev.to/poojang/i-have-idempotent-search-endpoints-doing-post-should-they-be-query-305a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — In June 2026, HTTP got a new method: &lt;strong&gt;QUERY&lt;/strong&gt; (&lt;a href="https://datatracker.ietf.org/doc/rfc10008/" rel="noopener noreferrer"&gt;RFC 10008&lt;/a&gt;). It's &lt;strong&gt;safe, idempotent, and cacheable like GET, but carries a request body like POST&lt;/strong&gt; — exactly the shape of every "search with a big filter payload" endpoint you've ever been forced to build as a &lt;code&gt;POST&lt;/code&gt;. It's real and standards-track. It is also barely deployable in a browser today (no &lt;code&gt;fetch&lt;/code&gt; support, needs a CORS preflight, and plenty of proxies still reject unknown methods). My verdict: &lt;strong&gt;model your API around it now, keep POST on the wire until the ecosystem catches up.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The endpoint I've apologized for a hundred times
&lt;/h2&gt;

&lt;p&gt;Every backend I've built has one endpoint that lies about what it is. On my multi-tenant food-tech platform it's the order search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /orders/search
{
  "outletIds": [12, 44, 91, 156],
  "status": ["preparing", "out_for_delivery"],
  "dateRange": { "from": "2026-06-01", "to": "2026-06-30" },
  "customer": { "phoneSuffix": "8821" },
  "sort": [{ "field": "placedAt", "dir": "desc" }],
  "page": 3, "pageSize": 50
}

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

&lt;/div&gt;



&lt;p&gt;That is a &lt;strong&gt;read&lt;/strong&gt;. It changes nothing. Call it a hundred times, get the same answer a hundred times. Semantically it is a &lt;code&gt;GET&lt;/code&gt;. But it's a &lt;code&gt;POST&lt;/code&gt;, and it's a &lt;code&gt;POST&lt;/code&gt; for one dumb, unavoidable reason: &lt;strong&gt;the filter doesn't fit in a URL.&lt;/strong&gt; Try to encode that nesting into a query string and you get an unreadable &lt;code&gt;?outletIds[]=12&amp;amp;outletIds[]=44&amp;amp;status[]=...&amp;amp;dateRange[from]=...&lt;/code&gt; mess that flirts with URL length limits, has no agreed-upon syntax for arrays and nested objects, and leaks a customer's phone digits into every proxy log, browser history entry, and &lt;code&gt;Referer&lt;/code&gt; header along the way.&lt;/p&gt;

&lt;p&gt;So we all reach for &lt;code&gt;POST&lt;/code&gt;. And the moment we do, we lie to the entire HTTP stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;We tell caches "this is dangerous, never cache it"&lt;/strong&gt; — even though it's the single most cacheable thing in the system. Our CDN and reverse proxy shrug and pass every identical search straight through to the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We tell every retry layer "do not automatically retry this"&lt;/strong&gt; — because &lt;code&gt;POST&lt;/code&gt; is defined as non-idempotent, meant for &lt;em&gt;creating&lt;/em&gt; things. So a dropped response on a pure read can't be safely re-sent by middleware, even though re-sending is completely harmless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same theme I keep hitting: HTTP semantics are load-bearing, and when you pick the wrong verb, you silently switch off the infrastructure built to help you. (It's the mirror image of the &lt;a href="https://poojan.technokari.com/blog/payment-webhook-source-of-truth" rel="noopener noreferrer"&gt;payment webhook post&lt;/a&gt; — there the problem was making a &lt;em&gt;write&lt;/em&gt; idempotent; here it's that a &lt;em&gt;read&lt;/em&gt; has been mislabeled as a write.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What QUERY actually is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://datatracker.ietf.org/doc/rfc10008/" rel="noopener noreferrer"&gt;RFC 10008&lt;/a&gt;, "The HTTP QUERY Method," was published as a Proposed Standard in &lt;strong&gt;June 2026&lt;/strong&gt;. It defines exactly the method that gap has needed since the beginning of HTTP. In the spec's own words, it's for cases where the "data conveyed is too voluminous to be encoded in the request's URI."&lt;/p&gt;

&lt;p&gt;Put my three verbs side by side and the hole QUERY fills is obvious:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;QUERY&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Safe (no state change)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotent (retry-safe)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cacheable&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✗ (effectively no)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Carries a request body&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;QUERY is &lt;strong&gt;safe&lt;/strong&gt; (the client isn't asking to change anything), &lt;strong&gt;idempotent&lt;/strong&gt; (it "can be automatically repeated or restarted without concern for partial state changes" — so retry layers are free to retry it), and &lt;strong&gt;cacheable&lt;/strong&gt;. The &lt;code&gt;POST /orders/search&lt;/code&gt; above becomes an honest &lt;code&gt;QUERY /orders&lt;/code&gt;, with the same JSON body, and every layer of the stack now understands what it's looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The genuinely new trick: a cache key that includes the body
&lt;/h2&gt;

&lt;p&gt;This is the part I find most interesting, and it's the reason QUERY isn't just "GET with a body" cosplay.&lt;/p&gt;

&lt;p&gt;For a &lt;code&gt;GET&lt;/code&gt;, the cache key is the URL. Simple. But a QUERY's meaningful input is in the &lt;em&gt;body&lt;/em&gt;, so RFC 10008 says the &lt;strong&gt;cache key MUST incorporate the request content and its metadata&lt;/strong&gt;. A reverse proxy or CDN looks at the &lt;code&gt;Content-Type&lt;/code&gt; and the body bytes &lt;em&gt;together&lt;/em&gt; and can serve a cached response for two identical queries.&lt;/p&gt;

&lt;p&gt;The spec even anticipates the obvious objection — "but &lt;code&gt;{"a":1,"b":2}&lt;/code&gt; and &lt;code&gt;{"b":2,"a":1}&lt;/code&gt; are the same query with different bytes." Caches are permitted to normalize away "semantically insignificant differences" (formatting, key order, content encoding) when &lt;em&gt;computing the key&lt;/em&gt;, without altering the request itself.&lt;/p&gt;

&lt;p&gt;That's a real capability GET-with-body never had and POST fundamentally can't have. My identical dashboard searches — same filter, fired by twenty operators every morning at the lunch-rush ramp-up — could be served from the edge instead of hammering Postgres. (I've written before about &lt;a href="https://poojan.technokari.com/blog/scaling-postgres-in-production" rel="noopener noreferrer"&gt;what that read load does to a database under peak&lt;/a&gt;; pushing repeat reads to a cache is precisely the pressure valve.)&lt;/p&gt;

&lt;h2&gt;
  
  
  So why haven't I migrated?
&lt;/h2&gt;

&lt;p&gt;Because "standardized" and "deployable" are two very different dates, and in July 2026 the gap between them is wide. Here's the honest state of the ecosystem, and it's the part the explainer posts skip:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browsers can't send it.&lt;/strong&gt; There is no &lt;code&gt;QUERY&lt;/code&gt; support in &lt;code&gt;fetch()&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt; today. MDN has no page for it, there's no &lt;code&gt;caniuse&lt;/code&gt; entry. If your caller is a web frontend, you literally cannot issue a QUERY from the browser right now. That alone rules it out for most user-facing traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It needs a CORS preflight.&lt;/strong&gt; QUERY is not on the CORS-safelist, so any cross-origin call triggers an &lt;code&gt;OPTIONS&lt;/code&gt; preflight round-trip. Not fatal, but it's latency and server config you don't pay for with a plain &lt;code&gt;GET&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The middle of the network is hostile to unknown methods.&lt;/strong&gt; Plenty of proxies, WAFs, API gateways, and legacy load balancers still reject or mangle any method they don't recognize. Your request has to survive every hop between client and origin, and some of those hops were configured in 2015.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Framework support is young and uneven.&lt;/strong&gt; It exists — &lt;a href="https://vensas.de/en/blog/http-query-method-dotnet-10" rel="noopener noreferrer"&gt;.NET 10 shipped client and server support&lt;/a&gt;, and various gateways that already allow custom methods can pass it through — but this is early-adopter territory, not "npm install and go."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You lose shareable URLs.&lt;/strong&gt; A filtered view expressed as a QUERY body can't be bookmarked, pasted into Slack, or deep-linked the way &lt;code&gt;?status=preparing&lt;/code&gt; can. For some search UIs that's a real downgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm actually doing about it
&lt;/h2&gt;

&lt;p&gt;Not a rewrite. A cheap bit of future-proofing that costs almost nothing and pays off the day the browser gap closes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Model the endpoint as a query, whatever verb carries it.&lt;/strong&gt; I'm making sure my search handler is a pure, side-effect-free function of its request body — no logging that mutates, no "record this search" write sneaking into the read path. If it's genuinely safe and idempotent &lt;em&gt;in behavior&lt;/em&gt;, flipping the wire method later is a one-line routing change, not a redesign. Same discipline as isolating &lt;a href="https://poojan.technokari.com/blog/payment-webhook-source-of-truth" rel="noopener noreferrer"&gt;order creation into one pure service function&lt;/a&gt;: keep the semantics clean and the transport swappable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Keep POST on the wire for now&lt;/strong&gt; , but treat that as a deployment detail, not the design. Where I control both ends (server-to-server, internal services, a CLI, my own SDK) I can experiment with real QUERY today, because there's no browser and often no hostile proxy in the path — that's where adoption will actually start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Set up cache keys to include the body deliberately&lt;/strong&gt; , so that when I do switch, the caching semantics RFC 10008 describes are something I've already reasoned about rather than a surprise.&lt;/p&gt;

&lt;p&gt;The Kreya team put the pragmatic rule best in &lt;a href="https://kreya.app/blog/new-http-query-method-explained/" rel="noopener noreferrer"&gt;their write-up&lt;/a&gt;: if there's no immediate need to change an endpoint, leave it be. QUERY isn't a migration you rush. It's a gap in HTTP that's finally being filled, and the right move is to stop &lt;em&gt;fighting&lt;/em&gt; the mismatch — stop pretending your reads are writes — so that adopting the correct method later is trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea to take away
&lt;/h2&gt;

&lt;p&gt;For years we've been shipping reads disguised as writes because HTTP gave us no honest option for "safe request, big body." &lt;strong&gt;QUERY is that option.&lt;/strong&gt; You can't put it on browser traffic yet — but you can stop building your search endpoints in a way that assumes the lie is permanent. Keep the behavior genuinely safe and idempotent, keep the body-as-input clean, and the day &lt;code&gt;fetch&lt;/code&gt; learns the verb, your migration is a routing table edit instead of a rewrite.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Reconciliation, Not Sync: Integrating a System That Also Webhooks You Back</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/reconciliation-not-sync-integrating-a-system-that-also-webhooks-you-back-2jcb</link>
      <guid>https://dev.to/poojang/reconciliation-not-sync-integrating-a-system-that-also-webhooks-you-back-2jcb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — When you integrate with a system that &lt;em&gt;also&lt;/em&gt; mutates the shared data and_also_ webhooks you when it changes, you cannot "keep them in sync." Both sides write, both sides notify, and you get echo loops, out-of-order updates, and fields that quietly diverge. The model that actually works is &lt;strong&gt;reconciliation&lt;/strong&gt; : (1) assign a single owner to every field so a change has an authoritative direction, (2) apply every inbound event &lt;strong&gt;idempotently&lt;/strong&gt; and out-of-order-safely, (3) tag your own writes so you can ignore the webhook they echo back, and (4) run a periodic &lt;strong&gt;full sweep&lt;/strong&gt; that compares both sides and repairs the delta. Live events keep you fresh; the sweep keeps you &lt;em&gt;correct&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The word "sync" is the bug
&lt;/h2&gt;

&lt;p&gt;The ticket says "sync our catalog with the POS." It sounds symmetric and simple. It is neither, and the word itself is what leads teams into a swamp.&lt;/p&gt;

&lt;p&gt;Picture the setup. Your platform has a menu — items, prices, availability. You integrate a point-of-sale system that the restaurant &lt;em&gt;also&lt;/em&gt; edits directly. Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The restaurant marks an item out of stock &lt;strong&gt;in the POS&lt;/strong&gt; → the POS webhooks &lt;em&gt;you&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;An operator marks the same item available &lt;strong&gt;in your dashboard&lt;/strong&gt; → you push to the POS.&lt;/li&gt;
&lt;li&gt;The POS applies your push → and webhooks you back that it changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two systems, both authoritative-feeling, both mutating, both notifying. "Sync" implies there is one true state and you're keeping a mirror of it. But there isn't one true state — there are two writers racing, and every write generates a notification that can trigger another write. The moment you try to make A always equal B by reacting to change events, you've built a distributed feedback loop. Let me show you the three ways it bites, because naming them is most of the cure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 1: the echo loop
&lt;/h2&gt;

&lt;p&gt;You receive a POS webhook: &lt;em&gt;item 88 is now unavailable&lt;/em&gt;. You dutifully write &lt;code&gt;available = false&lt;/code&gt; and, because your own writes are supposed to propagate, you push that change... back to the POS. The POS sees a write, and webhooks you: &lt;em&gt;item 88 changed&lt;/em&gt;. You apply it, and push again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [POS] ──"item 88 unavailable"──▶ [you: write + push back]
     ▲ │
     └──────────"item 88 changed"───────────────┘ ← infinite echo

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

&lt;/div&gt;



&lt;p&gt;Nothing is wrong with either system individually. The loop is &lt;em&gt;emergent&lt;/em&gt; — it exists only in the seam between them. In production this shows up as a column that flickers, a webhook log that never goes quiet, and rate-limit warnings from the POS at 2 a.m.&lt;/p&gt;

&lt;p&gt;The fix is to make your writes &lt;strong&gt;attributable&lt;/strong&gt;. When you apply an inbound change, tag it as originating from the POS and &lt;em&gt;do not&lt;/em&gt; echo it back out. Only changes that originate locally get pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyInbound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;last_source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// this change came FROM the pos&lt;/span&gt;
    &lt;span class="na"&gt;pos_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// remember what the pos told us (see Failure 2)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// note: NO push back to the POS. We do not propagate what the POS just told us.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;patch&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;last_source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local&lt;/span&gt;&lt;span class="dl"&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;pos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// only LOCAL-origin changes go outbound&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That single "who caused this change" tag breaks the loop. It's the integration version of a rule that keeps showing up: know who owns the write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 2: out-of-order and duplicate events
&lt;/h2&gt;

&lt;p&gt;Webhooks are delivered &lt;strong&gt;at-least-once and not necessarily in order&lt;/strong&gt;. The POS sends you "available → false" then "available → true" a second apart, and the network hands them to you &lt;em&gt;reversed&lt;/em&gt;. If you blindly apply the last one you processed, you end up with &lt;code&gt;false&lt;/code&gt; — permanently wrong, and nothing looks broken.&lt;/p&gt;

&lt;p&gt;You cannot fix this with timestamps you generate on receipt (they reflect arrival, not truth). You need a &lt;strong&gt;monotonic version from the source&lt;/strong&gt; and the rule "never apply an event older than what I've already applied":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyInbound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Idempotent AND order-safe: only move forward.&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos_version&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pos_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// stale or duplicate — we've already seen this or newer. No-op.&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;last_source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pos_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&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;If the source doesn't give you a version or updated-at you can trust, you're reduced to reconciliation-only (below) for that field — which is a perfectly honest answer, and better than pretending the event stream is ordered when it isn't. This is the same shape as making &lt;a href="https://poojan.technokari.com/blog/payment-webhook-source-of-truth" rel="noopener noreferrer"&gt;payment webhooks idempotent&lt;/a&gt;: the event stream is unreliable by contract, so correctness has to live in &lt;em&gt;how you apply&lt;/em&gt;, not in the events arriving perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 3: the fields don't mean the same thing
&lt;/h2&gt;

&lt;p&gt;This is the one that burns a week. Two systems rarely model the same concept identically, and the mismatches are silent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your platform has &lt;strong&gt;availability&lt;/strong&gt; (is this sellable right now?). The POS has &lt;strong&gt;stock count&lt;/strong&gt; &lt;em&gt;and&lt;/em&gt; a separate &lt;strong&gt;tracking mode&lt;/strong&gt; (is stock even being tracked?). An item with &lt;code&gt;stock = 0&lt;/code&gt; but &lt;code&gt;tracking = off&lt;/code&gt; is &lt;em&gt;available&lt;/em&gt;, not out of stock. Map "stock 0" straight to "unavailable" and you'll hide sellable items.&lt;/li&gt;
&lt;li&gt;The POS "deletes" an item, then "revives" it a day later under the same external id. If your import treats delete as terminal, the revive silently does nothing.&lt;/li&gt;
&lt;li&gt;A field you assumed was a boolean is a tri-state; a price you assumed was minor-units is major-units for one region; an id you keyed on gets reused.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no clever code for this. The fix is a written &lt;strong&gt;field ownership map&lt;/strong&gt; — for every field, exactly one system is the authority, and you translate meanings explicitly at the boundary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Catalog (name, description, category)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;POS never overrides; local is truth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;pushed to POS; inbound price events ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock / availability&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;POS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;POS is truth; translate &lt;code&gt;stock&lt;/code&gt;+&lt;code&gt;tracking&lt;/code&gt;→&lt;code&gt;available&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Item existence (create/delete/revive)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;POS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;delete is soft; a later revive must re-activate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once ownership is explicit, most "conflicts" evaporate — they were never conflicts, just two systems editing fields the other didn't own. A change to a platform-owned field ignores whatever the POS says about it, and vice versa. Write this table &lt;em&gt;before&lt;/em&gt; the code; it is the actual design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that makes it correct: the reconciliation sweep
&lt;/h2&gt;

&lt;p&gt;Everything above keeps you &lt;em&gt;fresh&lt;/em&gt; — reacting to live events. None of it keeps you &lt;em&gt;correct&lt;/em&gt;, because events get dropped. At-least-once is a floor: a webhook can fail every retry and vanish, a deploy can drop an in-flight batch, a bug can skip an apply. Over weeks, the two systems drift, and no single event tells you they have.&lt;/p&gt;

&lt;p&gt;So the load-bearing piece is a &lt;strong&gt;periodic full reconciliation&lt;/strong&gt; — a scheduled job that ignores the event stream entirely, pulls the full state of both sides, and repairs the delta by ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Runs on a schedule (this is a perfect job for a Postgres-backed&lt;/span&gt;
&lt;span class="c1"&gt;// outbox/cron worker — see the durable-queue post).&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&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;ours&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allByExternalId&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;theirs&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;pos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listAllItems&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// full snapshot, not events&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;extId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;posItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;theirs&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;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ours&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;extId&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;local&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="nf"&gt;createFromPos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posItem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// they have, we don't&lt;/span&gt;
    &lt;span class="c1"&gt;// Repair ONLY fields the other side owns:&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;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;translateAvailability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posItem&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&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;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;translateAvailability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;extId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;theirs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_source&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;flagOrphan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// we have it, POS doesn't — human decision, don't guess&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;Run it nightly (or hourly for volatile data). The live webhooks give you low-latency updates; the sweep guarantees that &lt;em&gt;even if every webhook for a day was lost&lt;/em&gt;, you converge to correct within one cycle. This is the &lt;a href="https://poojan.technokari.com/blog/postgres-outbox-instead-of-kafka" rel="noopener noreferrer"&gt;reconciliation-not-a-lucky-webhook idea&lt;/a&gt; applied to a whole integration: don't trust the stream to be complete — periodically check reality and repair.&lt;/p&gt;

&lt;p&gt;Two hard-won rules for the sweep: &lt;strong&gt;repair only fields the other side owns&lt;/strong&gt; (or two sweeps will fight the same field forever), and &lt;strong&gt;never auto-delete on "the other side doesn't have it"&lt;/strong&gt; — flag it for a human. A dropped event and a real deletion look identical from one snapshot; guessing wrong deletes a live menu item.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit everything at the boundary
&lt;/h2&gt;

&lt;p&gt;One operational note that paid for itself ten times over: &lt;strong&gt;log every inbound and outbound event, raw, before you interpret it.&lt;/strong&gt; When a price is wrong or an item vanished, the only way to answer "did they send us bad data, did we misapply good data, or did our push do it?" is to replay the boundary. A tiny append-only &lt;code&gt;integration_events&lt;/code&gt; table (direction, external id, raw payload, applied-result, timestamp) turns "no idea what happened" into a five-minute query. Integrations fail in the seam; instrument the seam.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually broke in production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A column-meaning mismatch marked good items unavailable.&lt;/strong&gt; We mapped the POS &lt;code&gt;stock&lt;/code&gt; field to availability without accounting for &lt;code&gt;tracking = off&lt;/code&gt;, so every untracked-but-sellable item went dark. Caught only because a restaurant called about missing menu items. Fix: the explicit translation function, plus a reconciliation pass that re-derived availability correctly for the whole catalog.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;delete-then-revive did nothing.&lt;/strong&gt; Our importer treated a POS delete as final and dropped the row's linkage; the next day's revive created no change because our code had stopped tracking the external id. Fix: soft-delete with the external id preserved, and treat a revive as reactivation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The echo loop we didn't notice for days&lt;/strong&gt; — because each hop was individually valid, only the POS's rate-limit emails revealed it. Fix: the &lt;code&gt;last_source&lt;/code&gt; tag, and a boundary log that made the loop visible at a glance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd still improve
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation is O(catalog) every run.&lt;/strong&gt; Fine at this scale, wasteful at 10×. I'd move to a hashed/versioned diff — compare a per-item fingerprint and only fetch full detail for mismatches — so the sweep cost tracks the &lt;em&gt;delta&lt;/em&gt;, not the total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ownership is still partly in my head.&lt;/strong&gt; It should be declarative config (a table of field → owner → translator) that both the live-apply path and the sweep read from, so they can never disagree about who owns what. Right now the two paths encode it separately, which is exactly the kind of drift this whole post is about.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one idea to take away
&lt;/h2&gt;

&lt;p&gt;You can never &lt;em&gt;sync&lt;/em&gt; two systems that both mutate and both notify — the symmetry is a lie that produces echo loops and silent divergence. You can only &lt;strong&gt;reconcile&lt;/strong&gt; : give every field one owner, apply inbound events idempotently and in-order-safely, tag your own writes so you don't echo them, and run a periodic sweep that treats reality — not the event stream — as the thing to converge to. Events keep you fast. Reconciliation keeps you right.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>I Automated 90% of a Grungy Data-Entry Job Without an LLM — and Where an Agent Would Actually Earn Its Place</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/i-automated-90-of-a-grungy-data-entry-job-without-an-llm-and-where-an-agent-would-actually-earn-1c7n</link>
      <guid>https://dev.to/poojang/i-automated-90-of-a-grungy-data-entry-job-without-an-llm-and-where-an-agent-would-actually-earn-1c7n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Onboarding a restaurant meant hand-entering hundreds of menu items and finding an image for each — hours of mind-numbing work per client. The 2026 reflex is "point an AI agent at it." I didn't. I built a &lt;strong&gt;deterministic pipeline&lt;/strong&gt; — bulk import, normalize, fuzzy-match images by filename, auto-apply the confident matches — that cleared ~90% of the work with code that is cheap, debuggable, and correct-by-construction. The remaining ~10% (genuinely ambiguous matches) goes to a &lt;strong&gt;human-in-the-loop&lt;/strong&gt; review queue. Then I mark the two specific spots where a vision-language model &lt;em&gt;does&lt;/em&gt; pay for itself. The lesson isn't "AI bad" — it's &lt;strong&gt;deterministic-first, AI where it's uniquely better, and never an agent for what a &lt;code&gt;for&lt;/code&gt; loop does more reliably.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The grungy job
&lt;/h2&gt;

&lt;p&gt;Every new restaurant on the platform arrives the same way: a spreadsheet (or a PDF, or a photo of a printed menu) with a few hundred items, and a folder of food photos with filenames like &lt;code&gt;paneer_tikka_final_v2.jpg&lt;/code&gt;. Someone has to create each item, set its price and category, and attach the right image. It's hours of tedious, error-prone clicking per client, and it's the least fun part of onboarding.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of task the current discourse says to hand to an autonomous agent: "let the AI read the menu, find the images, and fill in the system." And you &lt;em&gt;can&lt;/em&gt; build that. But before reaching for the most powerful, most expensive, least predictable tool available, it's worth asking what the job actually decomposes into. Most "AI-shaped" grunt work is 90% deterministic plumbing wearing a 10% judgment hat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: bulk import beats a smart importer
&lt;/h2&gt;

&lt;p&gt;The first instinct people automate away is data entry itself. But most of the input is already structured — a spreadsheet &lt;em&gt;is&lt;/em&gt; a table. It doesn't need intelligence; it needs a parser and validation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Parse the sheet, normalize, validate. No model. This is 60% of the "AI" job.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;importMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&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;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;normalizeName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// trim, collapse spaces, title-case&lt;/span&gt;
      &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parsePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// "₹120" | "120.00" | "120" → 12000 (minor units)&lt;/span&gt;
      &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;canonicalCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// map free-text → known category set&lt;/span&gt;
      &lt;span class="na"&gt;external_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;slugify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// stable key for matching (step 2)&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;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// missing price? unknown category?&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rowNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;problems&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// +2: header + 1-index, for human-readable errors&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;Everything a model would "understand" here — price formats, category names, whitespace — is a finite, enumerable set of cases. Enumerate them. The payoff: this code is &lt;strong&gt;deterministic and testable&lt;/strong&gt;. The same sheet produces the same output every run, a malformed price is a specific error on a specific row, and there's no token bill. An LLM doing this would be slower, non-reproducible, occasionally hallucinate a category, and cost money per run to do worse than a regex. Wrong tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: match images by filename — fuzzy, but still deterministic
&lt;/h2&gt;

&lt;p&gt;Attaching the right photo &lt;em&gt;sounds&lt;/em&gt; like it needs vision. Usually it doesn't, because the filenames already encode the answer: &lt;code&gt;paneer_tikka_final_v2.jpg&lt;/code&gt; is obviously the Paneer Tikka. This is a string-matching problem, not a seeing problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Score each image filename against each item name; auto-apply confident matches.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;matchImages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filenames&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;items&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;scored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filenames&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&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;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;slugify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;slugify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stripExt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&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="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&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;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// how clear was the winner?&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;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// confident → apply&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// ambiguous → human&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;results&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 important design choice is the &lt;strong&gt;second threshold&lt;/strong&gt; : not just "is the best match good?" but "is it &lt;em&gt;clearly&lt;/em&gt; better than the runner-up?" A high score with a tiny gap ("Chicken Biryani" vs "Chicken Biryani Special", both 0.9) is &lt;em&gt;ambiguous&lt;/em&gt;, and the right move is to not guess. That gap check is what makes auto-apply safe — it only fires when the answer is genuinely unambiguous, and everything else falls through to a human instead of being confidently wrong.&lt;/p&gt;

&lt;p&gt;Where filenames are missing or useless (&lt;code&gt;IMG_2231.jpg&lt;/code&gt;), a &lt;strong&gt;web image search by item name&lt;/strong&gt; fills the gap — fetch a few candidates, present them, still let a human confirm. Deterministic retrieval, human judgment on the final pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: a human-in-the-loop queue for the ambiguous 10%
&lt;/h2&gt;

&lt;p&gt;Here's the part the "full autonomy" pitch gets wrong: the last 10% isn't a failure of automation, it's &lt;em&gt;where the actual judgment lives&lt;/em&gt;, and a person resolves it faster and more accurately than a model guessing. So the pipeline's job is to &lt;strong&gt;shrink the human's work to only the decisions that need a human&lt;/strong&gt; — not to eliminate the human.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  spreadsheet ─▶ [import + validate] ─▶ [fuzzy match] ─┬─ auto (90%) ─▶ applied
                                                            └─ review (10%) ─▶ [human queue]
                                                                                     │
                                                                            pick candidate ─▶ applied

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

&lt;/div&gt;



&lt;p&gt;The review queue shows the item, the top 3–4 candidate images, and the validation problems — one screen, a few clicks each. A job that was "enter 300 items by hand" becomes "confirm 30 ambiguous matches." That's the win, and it came from ordinary code plus a good UI, not from a model.&lt;/p&gt;

&lt;p&gt;Two properties make this trustworthy, and they're the same ones I lean on everywhere: the pipeline is &lt;strong&gt;idempotent&lt;/strong&gt; (re-running on the same input doesn't create duplicates — it matches on &lt;code&gt;external_key&lt;/code&gt;, the same discipline behind &lt;a href="https://poojan.technokari.com/blog/payment-webhook-source-of-truth" rel="noopener noreferrer"&gt;idempotent order creation&lt;/a&gt;), and every auto-decision is &lt;strong&gt;logged with its score and gap&lt;/strong&gt; so a wrong auto-match is auditable and the threshold is tunable from real data.&lt;/p&gt;

&lt;h2&gt;
  
  
  So where does an LLM actually earn its place?
&lt;/h2&gt;

&lt;p&gt;I'm not arguing against AI here — I'm arguing against using the most expensive, least predictable tool for the parts a &lt;code&gt;for&lt;/code&gt; loop does better. There are exactly two spots in &lt;em&gt;this&lt;/em&gt; pipeline where a model is genuinely, uniquely better, and it's worth being precise about them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unstructured input the parser can't touch.&lt;/strong&gt; When the menu arrives as a &lt;em&gt;photo of a printed board&lt;/em&gt; or a messy PDF, there's no spreadsheet to parse — extracting "item, price, category" from an image is real multimodal understanding, and a &lt;strong&gt;vision-language model&lt;/strong&gt; does it well. This is the honest home for AI in this workflow: turning genuinely unstructured input into the structured rows my deterministic pipeline then takes over. The model does the perception; the boring code does the logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Semantic image disambiguation on the hard matches.&lt;/strong&gt; For the ambiguous 10%, a vision model &lt;em&gt;could&lt;/em&gt; look at the candidate photos and the item name and rank them better than filename similarity — "which of these three images is actually Paneer Tikka." That genuinely beats string matching on the residual. But note: it's operating on the pre-filtered hard cases, as an &lt;em&gt;assist to the human reviewer&lt;/em&gt;, not as an autonomous decider on all 300 items.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The pattern I'd stand behind: &lt;strong&gt;let a model do perception and semantics on the inputs that are truly unstructured or truly ambiguous, and let deterministic code own everything with a definite right answer.&lt;/strong&gt; That's the opposite of "an agent does the whole job." It's a model as a component with a narrow, well-defined contract — the same way you'd add any other dependency.&lt;/p&gt;

&lt;p&gt;A note on cost and reliability, since it's the part the demos skip: running a model over all 300 items, every re-import, is real latency and a real per-run bill for work a regex does for free and a hash does more reliably. Scoped to just the unstructured extraction and the ambiguous residual, the model runs on a tiny fraction of the volume — which is the difference between "AI feature that pays for itself" and "AI tax on a solved problem."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd build next (and what I still wouldn't)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Would build:&lt;/strong&gt; the vision-model extraction path for photo/PDF menus, feeding the &lt;em&gt;same&lt;/em&gt; deterministic pipeline. That's a clean, high-value addition with a narrow contract — extract rows, hand off to code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Would build:&lt;/strong&gt; learn the thresholds from the human queue. Every time a reviewer overrides an auto-match or confirms an ambiguous one, that's labeled data to tune the 0.85/0.15 cutoffs — no model required, just feedback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still wouldn't build:&lt;/strong&gt; a fully autonomous "agent" that owns the whole flow end-to-end. It would be less reliable, harder to debug, more expensive, and non-reproducible — trading a pipeline I can reason about for a black box I'd have to babysit. The 10% that needs judgment is cheaper and safer with a human than with an agent pretending to be one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one idea to take away
&lt;/h2&gt;

&lt;p&gt;Most "AI could do this" grunt work is a deterministic pipeline with a small core of real judgment. &lt;strong&gt;Automate the deterministic 90% with boring, testable, idempotent code; route the ambiguous 10% to a human; and spend an LLM only where it's uniquely better — perception on unstructured input, semantics on the genuinely hard residual.&lt;/strong&gt; That's not AI-skepticism. It's using the right tool for each part of the job — which is the whole skill.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>data</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Never Trust the Client to Create the Order: Making Payment the Source of Truth</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/never-trust-the-client-to-create-the-order-making-payment-the-source-of-truth-oee</link>
      <guid>https://dev.to/poojang/never-trust-the-client-to-create-the-order-making-payment-the-source-of-truth-oee</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — If order creation happens on the client &lt;em&gt;after&lt;/em&gt; payment, a crashed phone means money captured with no order. Move creation to the &lt;code&gt;payment.captured&lt;/code&gt;webhook so the durable, retrying, server-side actor owns the write. That creates a client-vs-webhook race, which takes three layers to kill: a &lt;code&gt;UNIQUE(txn_id)&lt;/code&gt;constraint (the DB has the final say), an "order already exists" guard, and treating duplicate webhooks as expected no-ops. Verify signatures, always &lt;code&gt;200&lt;/code&gt;what you won't act on, and ship it behind a flag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The bug that doesn't show up in your tests
&lt;/h2&gt;

&lt;p&gt;Here's the order flow almost every food app ships first, because it's the obvious one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer taps &lt;strong&gt;Pay&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The payment gateway's SDK opens, they pay, it succeeds.&lt;/li&gt;
&lt;li&gt;The SDK returns control to your app.&lt;/li&gt;
&lt;li&gt;Your app calls &lt;code&gt;POST /createOrder&lt;/code&gt; with the cart.&lt;/li&gt;
&lt;li&gt;The order lands in the database. The kitchen sees it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every step works. The demo works. The staging test works. And then you go to production, and a handful of times a day, a customer's money leaves their account and &lt;strong&gt;no order is ever created&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Look at the flow again. The order is created in &lt;strong&gt;step 4&lt;/strong&gt; — &lt;em&gt;by the client, after payment&lt;/em&gt;. Everything between "money captured" and "order written" is running on a phone you don't control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app crashes right after the gateway returns.&lt;/li&gt;
&lt;li&gt;The OS kills the backgrounded app before step 4 fires.&lt;/li&gt;
&lt;li&gt;The customer's train goes into a tunnel and the &lt;code&gt;/createOrder&lt;/code&gt; request never lands.&lt;/li&gt;
&lt;li&gt;Someone force-quits because the spinner "looked stuck."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payment is real. The order is not. You've built a &lt;strong&gt;dual-write&lt;/strong&gt; : two systems (the payment provider and your database) that must both commit, coordinated by the least reliable computer in the entire system — the customer's phone. There is no transaction spanning both. When the second write is skipped, you don't get an error. You get silence, and a support ticket that reads "I paid and nothing happened."&lt;/p&gt;

&lt;p&gt;You cannot fix this by adding retries on the client. The client is the thing that's gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: make the money the source of truth
&lt;/h2&gt;

&lt;p&gt;The payment provider already knows something happened — that's what a &lt;strong&gt;webhook&lt;/strong&gt; is for. When a payment is captured, the gateway makes a server-to-server call to a URL you own. That call doesn't depend on the customer's phone at all. It retries on its own if your server is briefly down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BEFORE — client is responsible for the write that matters
                                          ✗ crash / tunnel / OS-kill
  [phone] ──pay──▶ [gateway] ──ok──▶ [phone] ──✗──▶ [POST /createOrder]
                                                          money taken, no order

AFTER — durable server-side actor owns the write
  [phone] ──pay──▶ [gateway] ──payment.captured──▶ [your webhook] ──▶ createOrder()
     ▲ (retries on its own) │
     └────────────── order_confirmed (push) ─────────────────────────────┘

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

&lt;/div&gt;



&lt;p&gt;So the order should be created there — on the &lt;code&gt;payment.captured&lt;/code&gt; webhook, server-side — not on the client's say-so.&lt;/p&gt;

&lt;p&gt;The one thing this requires: at the moment you &lt;em&gt;initiate&lt;/em&gt; payment, you have to stash everything needed to build the order somewhere durable, so the webhook can reconstruct it later without the client. I persist the entire order payload onto the transaction row when payment starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// At payment initiation — BEFORE the customer pays.&lt;/span&gt;
&lt;span class="c1"&gt;// The order doesn't exist yet, but everything to build it is now durable.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;txn_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;txnId&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="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payment_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;order_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the full cart, exactly as /createOrder expects it&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customerId&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;Now the webhook has everything. The client's job shrinks to "start the payment and then listen for a push telling you the order exists." It is no longer &lt;em&gt;responsible&lt;/em&gt; for the order's existence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The webhook the payment provider calls. No phone involved.&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/payment/order-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Verify it's really from the gateway (see "the boring parts" below)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-signature&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;event&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;event&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment.captured&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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="c1"&gt;// ack, don't act&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;txn&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByTxnId&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="nx"&gt;txn_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Build the order from the payload we stashed at initiation&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="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Push to the customer's app — it's been waiting, not driving&lt;/span&gt;
  &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order_confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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;That's the whole idea in five lines. The interesting part is everything that idea &lt;em&gt;breaks&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One order, two callers, zero divergence
&lt;/h2&gt;

&lt;p&gt;The moment you have a webhook creating orders, you have &lt;strong&gt;two&lt;/strong&gt; things that create orders: the webhook, and the old client route (which you can't just delete — cash orders, wallet orders, and manual/counter orders still come in through it, and you want a staged rollout, not a big-bang cutover).&lt;/p&gt;

&lt;p&gt;Two code paths that build the same object will drift. One will validate stock and the other won't. One will apply the subsidy math slightly differently. Six weeks later they disagree about what a valid order even is, and you're debugging why webhook orders have a different total than client orders.&lt;/p&gt;

&lt;p&gt;The fix is to make "create an order" exactly &lt;strong&gt;one&lt;/strong&gt; function — pure business logic, no HTTP, no framework objects, callable from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One function. No req, no res. Throws typed errors so any caller&lt;/span&gt;
&lt;span class="c1"&gt;// can map them to its own transport (HTTP status, socket error, log).&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;txnId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;orderError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VALIDATION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;txnId is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;orderError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VALIDATION&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;total must be positive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validateAndFetchItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// stock, availability&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outlet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchOutlet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outletId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;enforceMinimumOrderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outlet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// server-authoritative&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pricing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applySubsidies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outlet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// same math, always&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildOrderRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pricing&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;orderError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// type ∈ {'VALIDATION','NOT_FOUND','INTERNAL'} → caller maps to 400/404/500&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;type&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;e&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 HTTP route becomes a thin adapter — parse the request, call the service, map the typed error to a status code. The webhook is another adapter that does the same, minus the response body. &lt;strong&gt;The order semantics live in exactly one place&lt;/strong&gt; , and both entry points are provably identical because they are literally the same function call.&lt;/p&gt;

&lt;p&gt;A quiet but important detail: things like the minimum-order-value check and the pricing/subsidy math live &lt;em&gt;inside&lt;/em&gt; this service, not in the client. The client can compute a total to show the user, but the number that hits the database is computed server-side, every time. Once the server owns order creation, it should own every rule that decides whether the order is valid — otherwise a modified client is a discount generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The race you just created
&lt;/h2&gt;

&lt;p&gt;Here's the trap. You didn't replace the client flow — you added the webhook alongside it. So on a normal, healthy order, &lt;strong&gt;both fire&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The client's &lt;code&gt;/createOrder&lt;/code&gt; lands (the happy path still works fine).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;payment.captured&lt;/code&gt; webhook also lands, a second or two later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two writers, same order, racing. Without a guard you get &lt;strong&gt;two orders for one payment&lt;/strong&gt; — which is strictly worse than the bug you started with, because now the kitchen makes the dish twice and settlement is wrong.&lt;/p&gt;

&lt;p&gt;The tempting fix is "check if the order exists, and if not, create it." That check-then-act is itself a race: both callers can check "no order" in the same instant and both proceed. You cannot fix a concurrency bug with an &lt;code&gt;if&lt;/code&gt; statement in application code.&lt;/p&gt;

&lt;p&gt;So I layered three guards, weakest to strongest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1 — the database has the final say
&lt;/h3&gt;

&lt;p&gt;Every order carries the payment's transaction id. A single unique constraint makes a second order for the same payment &lt;em&gt;physically impossible&lt;/em&gt;, no matter how the code races:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;orders_txn_id_unique&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txn_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Postgres allows multiple &lt;code&gt;NULL&lt;/code&gt;s in a unique column, so orders that legitimately have no payment yet (pending, cash-on-delivery being assembled) are unaffected. Whoever inserts second gets a constraint violation instead of a duplicate. This is the backstop that does not depend on me being clever — the other two layers just make it rare and graceful instead of an exception in the logs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before you can &lt;em&gt;add&lt;/em&gt; this constraint, you have to prove there are no existing duplicates. That one-line &lt;code&gt;GROUP BY txn_id HAVING COUNT(*) &amp;gt; 1&lt;/code&gt; is not a formality — run it, and you'll often find the historical duplicates the bug already created. Fix those first, then constrain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Layer 2 — the webhook recognizes "the client already won"
&lt;/h3&gt;

&lt;p&gt;Before creating anything, the webhook checks whether an order for this payment already exists. If it does, the client beat it here — so the webhook does &lt;em&gt;not&lt;/em&gt; create a second order. It just reconciles the payment status and re-notifies the customer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByTxnId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txnId&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;existing&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txnId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// just reconcile the money&lt;/span&gt;
  &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order_confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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;h3&gt;
  
  
  Layer 3 — duplicate &lt;em&gt;webhooks&lt;/em&gt; are expected, not exceptional
&lt;/h3&gt;

&lt;p&gt;Payment providers guarantee &lt;strong&gt;at-least-once&lt;/strong&gt; delivery. They will send you the same &lt;code&gt;payment.captured&lt;/code&gt; twice, and if your endpoint is slow to &lt;code&gt;200&lt;/code&gt;, they'll retry it as a matter of policy. So "I've seen this transaction already" has to be a no-op, not a second order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;txn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// We already fully processed this one. Re-emit the confirmation&lt;/span&gt;
  &lt;span class="c1"&gt;// (the customer may have reconnected) and ack. Do not re-create.&lt;/span&gt;
  &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order_confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;existing&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&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;And every status write is conditional — &lt;code&gt;UPDATE ... WHERE status = 'pending'&lt;/code&gt; — so replaying a webhook can never move a row backward. Conditional updates are the cheapest idempotency tool there is; use them everywhere a webhook writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring parts that aren't optional
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Verify the signature.&lt;/strong&gt; Your order-creation endpoint is now a public URL that creates orders. Anyone who finds it could POST a fake &lt;code&gt;payment.captured&lt;/code&gt; and get free food. Every provider signs its webhooks — an HMAC of the raw body against a shared secret. Verify it against the &lt;strong&gt;raw&lt;/strong&gt; bytes, before any parsing, and reject on mismatch. This is the one place I return &lt;code&gt;400&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return &lt;code&gt;200&lt;/code&gt; for everything you're not going to act on.&lt;/strong&gt; Wrong event type? &lt;code&gt;200&lt;/code&gt;. Not a payment you care about? &lt;code&gt;200&lt;/code&gt;. Feature flag off? &lt;code&gt;200&lt;/code&gt;. A non-2xx tells the provider "retry me," and a webhook that returns &lt;code&gt;500&lt;/code&gt; on messages it simply doesn't handle will get itself hammered by the retry policy it triggered. Acknowledge fast; act only on what's yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship it behind a flag.&lt;/strong&gt; This changes how &lt;em&gt;money becomes orders&lt;/em&gt; — the single most sensitive path in the system. I put the whole webhook flow behind an environment flag, defaulted off, so the existing client flow was untouched until I was ready. Even disabled, the endpoint returns &lt;code&gt;200&lt;/code&gt; so the provider isn't building a retry backlog while I'm still testing. Flip it on for one outlet, watch, widen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually broke in production
&lt;/h2&gt;

&lt;p&gt;I'd be lying if I framed this as a clean win. Moving the source of truth to the server surfaced a class of bug that the client flow had been hiding: &lt;strong&gt;status reconciliation for the paths that don't go through the gateway at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cash and counter/manual orders never produce a &lt;code&gt;payment.captured&lt;/code&gt; webhook — there's no online payment to capture. Their transaction rows were being created as &lt;code&gt;pending&lt;/code&gt; and then... nothing flipped them to &lt;code&gt;success&lt;/code&gt;, because the thing that flips them is the webhook that never fires for cash. For a while, revenue reports undercounted because a slice of perfectly-good cash orders looked unpaid.&lt;/p&gt;

&lt;p&gt;The fix was two-part, and it's the honest shape of most production work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Forward fix&lt;/strong&gt; — the code that creates a manual/cash order now writes &lt;code&gt;status = 'success'&lt;/code&gt; at creation time, since for cash there is no later confirmation step to wait for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backfill&lt;/strong&gt; — a migration to reconcile the rows already stuck in &lt;code&gt;pending&lt;/code&gt;, run as &lt;em&gt;preview first&lt;/em&gt; (a read-only &lt;code&gt;SELECT&lt;/code&gt; of exactly what will change), then the &lt;code&gt;UPDATE&lt;/code&gt; inside a transaction I could &lt;code&gt;ROLLBACK&lt;/code&gt;. You do not run a bare &lt;code&gt;UPDATE&lt;/code&gt; against a money table and hope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson I keep relearning: when you make one path authoritative, you inherit responsibility for every path that &lt;em&gt;isn't&lt;/em&gt; on it. The webhook made online orders bulletproof and immediately made me care about the orders that have no webhook.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd still improve
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The client flow is still allowed to write orders.&lt;/strong&gt; Long-term, I'd rather the client only ever &lt;em&gt;initiates&lt;/em&gt; and the server is the sole creator for online payments too — collapsing three idempotency layers back toward one. The layers exist because I'm running both flows during migration; they're a bridge, not the destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation should be a scheduled job, not a lucky webhook.&lt;/strong&gt; At-least-once is a floor, not a ceiling — a provider can drop a webhook entirely. A periodic sweep that asks the gateway "what did you capture in the last hour that I have no order for?" turns "we lost an order" from an incident into a self-healing background task. That's the next thing I'm building.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one idea to take away
&lt;/h2&gt;

&lt;p&gt;If two systems must both commit and no transaction spans them, &lt;strong&gt;do not let the unreliable one go second.&lt;/strong&gt; Put the durable, retrying, server-side actor in charge of the write that matters, stash everything it needs ahead of time, and assume it will be told to do the job more than once. Everything above is just the working-out of that one sentence.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>fintech</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>You Probably Don't Need Kafka: A Durable Outbox and Job Queue in Plain Postgres</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/you-probably-dont-need-kafka-a-durable-outbox-and-job-queue-in-plain-postgres-4791</link>
      <guid>https://dev.to/poojang/you-probably-dont-need-kafka-a-durable-outbox-and-job-queue-in-plain-postgres-4791</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — The moment you "save the order, then publish an event / send an email / call a downstream service," you have a &lt;strong&gt;dual write&lt;/strong&gt; : two systems that must both commit with no transaction spanning them. Kafka doesn't fix that — it just gives you a second system to keep in sync. The fix is a &lt;strong&gt;transactional outbox&lt;/strong&gt; : write the job into the &lt;em&gt;same Postgres transaction&lt;/em&gt; as your business data, then drain it with a worker that dequeues using &lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt;. You get durable, at-least-once delivery with ordered, concurrent workers — and because delivery is at-least-once, your handlers must be idempotent. For a huge class of apps that is the entire messaging stack, and it's a table you already know how to back up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The email that sends twice, or never
&lt;/h2&gt;

&lt;p&gt;Here's the feature: when an order is placed, send the customer a confirmation. The obvious code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&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="c1"&gt;// 1. commit the order&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendConfirmation&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="c1"&gt;// 2. call the email provider&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Two writes to two systems, and no transaction wraps both. So it fails in two directions, and both happen in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Insert commits, then the process dies&lt;/strong&gt; (deploy, OOM, the email API times out) → order exists, email never sends. Silent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email sends, then the insert rolls back&lt;/strong&gt; (a constraint trips on commit) → customer gets "your order is confirmed" for an order that doesn't exist. Worse than silent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same &lt;strong&gt;dual-write problem&lt;/strong&gt; I hit moving &lt;a href="https://poojan.technokari.com/blog/payment-webhook-source-of-truth" rel="noopener noreferrer"&gt;order creation behind the payment webhook&lt;/a&gt; — two systems that must both commit, coordinated by nothing. And the instinct a lot of teams reach for here is: "add a message broker." Put Kafka (or SQS, or RabbitMQ) between the order and the email, publish an event, let a consumer send the mail.&lt;/p&gt;

&lt;p&gt;But look at what that actually does to the failure above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&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="c1"&gt;// commit to Postgres&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;kafka&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.placed&lt;/span&gt;&lt;span class="dl"&gt;'&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="c1"&gt;// commit to Kafka — STILL a second system&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You didn't remove the dual write. You &lt;strong&gt;moved&lt;/strong&gt; it. Now the two systems that can disagree are Postgres and Kafka, and you've taken on a broker to run, monitor, upgrade, secure, and reason about — to solve a problem it doesn't actually solve. If the process dies between those two lines, the event is lost exactly like the email was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The outbox: make the message part of the transaction
&lt;/h2&gt;

&lt;p&gt;The insight is small and it's the whole thing: &lt;strong&gt;if the message lived in the same database as the business data, one transaction could commit both — atomically.&lt;/strong&gt; No coordination problem, because there's only one system.&lt;/p&gt;

&lt;p&gt;So you don't publish to a broker inside your request. You insert a row into an &lt;code&gt;outbox&lt;/code&gt; table, in the &lt;em&gt;same&lt;/em&gt; transaction as the order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;job_type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- 'send_confirmation', 'push_to_pos', ...&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;-- pending | done | dead&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;run_after&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;-- for retry backoff&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- The index that makes the dequeue cheap: only ever scan runnable rows.&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_outbox_runnable&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="k"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;One&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;two&lt;/span&gt; &lt;span class="n"&gt;inserts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&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="n"&gt;const&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderRow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="n"&gt;job_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'send_confirmation'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customerId&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="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="n"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;durably&lt;/span&gt; &lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;rolls&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;there&lt;/span&gt; &lt;span class="n"&gt;was&lt;/span&gt; &lt;span class="k"&gt;no&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;no&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;They&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="n"&gt;never&lt;/span&gt; &lt;span class="n"&gt;disagree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That's the trick. The message is now as durable and as consistent as the order itself, because it &lt;em&gt;is&lt;/em&gt; the order's transaction. Nothing is published to anyone yet — a separate worker does that, on its own schedule, after the commit is real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DUAL WRITE — two systems, no shared transaction, they drift
  [request] ──insert order──▶ [Postgres]
             └──publish event──▶ [Kafka] ✗ dies here → event lost

OUTBOX — one transaction owns both, a worker drains later
  [request] ──┐
                ├─(one tx)─▶ [Postgres: orders + outbox]
  order + job ──┘ │
                            [worker] ◀──┘ polls, sends, marks done
                                 │
                                 └──▶ email / POS / downstream

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Draining the queue: &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Now the worker. You want several workers running for throughput, and you want them to &lt;em&gt;never&lt;/em&gt; grab the same job. The naive "select pending, then update" has the classic race: two workers read the same row and both process it.&lt;/p&gt;

&lt;p&gt;Postgres has a purpose-built answer, and it's the same primitive behind &lt;a href="https://poojan.technokari.com/blog/concurrency-safe-delivery-dispatcher" rel="noopener noreferrer"&gt;the atomic delivery dispatcher I wrote about&lt;/a&gt;: &lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt;. It says "lock the rows I'm taking, and &lt;em&gt;skip&lt;/em&gt; any row another worker has already locked instead of blocking on it." Concurrent workers glide past each other and each pulls a disjoint batch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;drainBatch&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tx&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;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Claim up to N runnable jobs; other workers skip these locked rows.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobs&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
      SELECT id, job_type, payload, attempts
      FROM outbox
      WHERE status = 'pending' AND run_after &amp;lt;= now()
      ORDER BY id
      FOR UPDATE SKIP LOCKED
      LIMIT 20
    `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for &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;job&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&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;handlers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job_type&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// do the side effect&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// status = 'done'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="nf"&gt;backoffOrDie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// retry with delay, or park it&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;Run &lt;code&gt;drainBatch&lt;/code&gt; on an interval (or, better, &lt;code&gt;LISTEN&lt;/code&gt;/&lt;code&gt;NOTIFY&lt;/code&gt; to wake instantly on new rows and poll as a fallback). Run it in three processes and you have three concurrent, non-overlapping workers with zero extra infrastructure. The &lt;code&gt;ORDER BY id&lt;/code&gt; gives you rough FIFO; the &lt;code&gt;LIMIT&lt;/code&gt; bounds how much one worker holds at once.&lt;/p&gt;

&lt;p&gt;Retry and backoff are just column math — no broker "redelivery" semantics to configure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;backoffOrDie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;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="s1"&gt;dead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// dead-letter, alert on it&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delaySec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// exponential, capped at 1h&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;run_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`now() + &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;delaySec&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; * interval '1 second'`&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;h2&gt;
  
  
  The catch you must design for: at-least-once
&lt;/h2&gt;

&lt;p&gt;Here's the honest part the "just use Postgres" posts gloss over. This queue is &lt;strong&gt;at-least-once&lt;/strong&gt; , not exactly-once — and &lt;em&gt;no&lt;/em&gt; queue, Kafka included, gives you true exactly-once delivery of a side effect. The reason is unavoidable: a worker can send the email, and then die &lt;em&gt;before&lt;/em&gt; it writes &lt;code&gt;status = 'done'&lt;/code&gt;. The row is still &lt;code&gt;pending&lt;/code&gt;, so the next worker sends the email again.&lt;/p&gt;

&lt;p&gt;You cannot close that window by being clever with ordering — "mark done, then send" just flips the failure to "marked done, never sent," which is worse. The window is fundamental. So you don't try to eliminate duplicates. You make &lt;strong&gt;duplicates harmless&lt;/strong&gt; by requiring every handler to be idempotent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sending email / push:&lt;/strong&gt; dedupe on a natural key (&lt;code&gt;order_id + 'confirmation'&lt;/code&gt;) so the second send is a no-op.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calling a downstream API:&lt;/strong&gt; pass an idempotency key so the provider collapses retries — the same discipline the &lt;a href="https://poojan.technokari.com/blog/payment-webhook-source-of-truth" rel="noopener noreferrer"&gt;payment webhook relies on&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing to your own DB:&lt;/strong&gt; a &lt;code&gt;UNIQUE&lt;/code&gt; constraint or a conditional &lt;code&gt;UPDATE ... WHERE status = 'pending'&lt;/code&gt; so a replay can't double-apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Assume every job runs more than once, and make that fine" is the price of admission for at-least-once delivery. It's also, not coincidentally, exactly the discipline that makes a system resilient to &lt;em&gt;any&lt;/em&gt; retry — client retries, load-balancer retries, a human clicking twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety net: reconciliation, not just delivery
&lt;/h2&gt;

&lt;p&gt;The outbox guarantees a job is &lt;em&gt;durably recorded&lt;/em&gt;. It does not, on its own, guarantee the downstream truth matches yours forever — a handler can succeed on your side and the downstream can still lose it, or a bug can park jobs as &lt;code&gt;dead&lt;/code&gt; and no one notices. So the pattern isn't complete without a periodic &lt;strong&gt;reconciliation sweep&lt;/strong&gt; that checks reality against your records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A cron that runs every few minutes — the backstop behind the queue.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Jobs that have been 'pending' far too long → the worker is wedged. Alert.&lt;/span&gt;
  &lt;span class="c1"&gt;// 2. Anything in 'dead' → surface it; a human or a fixed handler must resolve it.&lt;/span&gt;
  &lt;span class="c1"&gt;// 3. For integrations you can query back: ask the downstream&lt;/span&gt;
  &lt;span class="c1"&gt;// "what do you have that I don't, and vice versa?" and repair the delta.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This is the "scheduled job, not a lucky webhook" idea I closed the payment-webhook post promising to build — and it generalizes. At-least-once is a &lt;em&gt;floor&lt;/em&gt;, not a ceiling: a message can still be dropped entirely by something outside your control. A reconciliation loop turns "we silently lost one" from an incident your customers report into a background task that self-heals. (I go deeper on this in &lt;a href="https://poojan.technokari.com/blog/reconciliation-not-sync" rel="noopener noreferrer"&gt;Reconciliation, Not Sync&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually broke in production
&lt;/h2&gt;

&lt;p&gt;Four things bit me, and they're the four every outbox owner eventually meets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Poison jobs wedged a worker.&lt;/strong&gt; One malformed payload threw every time, and because I retried forever, that job's row got picked, failed, and re-queued in a hot loop that starved the batch. The fix is the &lt;code&gt;dead&lt;/code&gt; state above: a hard attempt cap that parks the job and &lt;em&gt;alerts&lt;/em&gt;, instead of retrying into infinity. A dead-letter you don't monitor is just a slower silence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;done&lt;/code&gt; rows piled up and the index bloated.&lt;/strong&gt; The &lt;code&gt;outbox&lt;/code&gt; table grew to millions of &lt;code&gt;done&lt;/code&gt; rows and the runnable-rows query slowly got heavier. Two fixes: the &lt;em&gt;partial&lt;/em&gt; index (&lt;code&gt;WHERE status = 'pending'&lt;/code&gt;) so the index only tracks live work regardless of table size, and a nightly job that deletes or archives &lt;code&gt;done&lt;/code&gt; rows older than a few days. Treat the outbox as a spool, not a permanent log.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I reached for &lt;code&gt;LIMIT&lt;/code&gt; without &lt;code&gt;SKIP LOCKED&lt;/code&gt; first&lt;/strong&gt; and workers serialized — each blocked waiting for the other's locked row instead of skipping it. Throughput collapsed to single-worker under contention. &lt;code&gt;SKIP LOCKED&lt;/code&gt; is not optional; it's the entire reason this scales past one worker.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A slow handler held its transaction open too long.&lt;/strong&gt; Because the claim-and-process ran inside one transaction, a handler that made a 30-second external call held row locks (and a connection) for 30 seconds. For slow side effects, claim the job in a short transaction (flip it to &lt;code&gt;processing&lt;/code&gt; with a lease/&lt;code&gt;locked_until&lt;/code&gt; timestamp), commit, &lt;em&gt;then&lt;/em&gt; do the slow work outside the lock, and mark &lt;code&gt;done&lt;/code&gt; in a second short transaction. Keep transactions that hold locks short — a lesson that shows up everywhere, including &lt;a href="https://poojan.technokari.com/blog/scaling-postgres-in-production" rel="noopener noreferrer"&gt;scaling the database itself under load&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When you actually do need Kafka
&lt;/h2&gt;

&lt;p&gt;I'm not anti-Kafka; I'm anti-&lt;em&gt;premature&lt;/em&gt;-Kafka. Reach for a real broker when you genuinely outgrow a table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Throughput&lt;/strong&gt; in the hundreds-of-thousands of messages per second, sustained — past what one Postgres instance should be spooling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan-out to many independent consumer groups&lt;/strong&gt; that each need their own replayable offset into a durable log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event sourcing / stream processing&lt;/strong&gt; where the log itself is the source of truth and you replay history to rebuild state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-team decoupling&lt;/strong&gt; where dozens of services subscribe and you want the broker as the contract boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're sending a confirmation email, pushing an order to a POS, or kicking off a report — none of those are on that list. You have one app and one database. The outbox is less code, one fewer system at 3 a.m., and it's backed up by the same &lt;code&gt;pg_dump&lt;/code&gt; you already run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea to take away
&lt;/h2&gt;

&lt;p&gt;The dual-write problem doesn't get solved by adding a second system — it gets solved by making the message part of a transaction you already commit. &lt;strong&gt;Write the job into Postgres with your data, drain it with &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;, make every handler idempotent because delivery is at-least-once, and put a reconciliation sweep behind it.&lt;/strong&gt; That's a durable, concurrent, retrying job queue in about forty lines and one table — and for most products, it's the whole messaging stack you were about to go operate a broker for.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>postgres</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Scaling PostgreSQL in Production: Indexing, RPC Functions, Views, and Query Optimization</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Fri, 05 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/scaling-postgresql-in-production-indexing-rpc-functions-views-and-query-optimization-3j05</link>
      <guid>https://dev.to/poojang/scaling-postgresql-in-production-indexing-rpc-functions-views-and-query-optimization-3j05</guid>
      <description>&lt;p&gt;As applications grow, database performance becomes one of the biggest factors affecting user experience. Features can be implemented quickly, but poorly optimized queries, inefficient data access patterns, and increasing data volume eventually create bottlenecks.&lt;/p&gt;

&lt;p&gt;Recently, I worked on optimizing a multi-tenant PostgreSQL system built on Supabase. The system consisted of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;99 database tables&lt;/li&gt;
&lt;li&gt;Nearly 1 million records in the largest table&lt;/li&gt;
&lt;li&gt;More than 120,000 database requests per day&lt;/li&gt;
&lt;li&gt;Complex reporting and dashboard requirements&lt;/li&gt;
&lt;li&gt;Transactional workloads with strict consistency requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While the application continued to function correctly, several reporting and aggregation queries had started taking multiple seconds to execute. As the dataset grew, it became clear that improvements were needed to keep the system responsive and maintainable.&lt;/p&gt;

&lt;p&gt;This article covers the techniques that produced the biggest impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Bottlenecks
&lt;/h2&gt;

&lt;p&gt;The application served multiple tenants from a shared database. Most operations were tenant-scoped and involved data spread across several related tables.&lt;/p&gt;

&lt;p&gt;The primary performance challenges were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow reporting queries&lt;/li&gt;
&lt;li&gt;Repeated aggregation logic&lt;/li&gt;
&lt;li&gt;Increasing dashboard generation time&lt;/li&gt;
&lt;li&gt;Large result sets being fetched unnecessarily&lt;/li&gt;
&lt;li&gt;Excessive application-side processing&lt;/li&gt;
&lt;li&gt;Growing query complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than attempting a complete redesign, the focus was on improving the existing architecture through targeted database optimizations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Composite Indexes Instead of More Indexes
&lt;/h2&gt;

&lt;p&gt;One of the first discoveries was that several frequently executed queries were filtering on multiple columns simultaneously.&lt;/p&gt;

&lt;p&gt;A common mistake is creating separate indexes for every column and expecting PostgreSQL to combine them efficiently via index merging. In practice, composite indexes often provide significantly better results when they match actual query patterns because they allow Postgres to perform a single index scan to satisfy multiple filters.&lt;/p&gt;

&lt;p&gt;For example, instead of indexing columns independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ❌ Suboptimal: Individual index on each column&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_events_tenant_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_events_user_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_events_event_date&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;we created a single composite index tailored to our access patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ✅ Optimal: Single composite index matching query filters&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_events_tenant_user_date&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Similarly, transaction-related queries benefited from composite indexes structured like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_transactions_composite&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transaction_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;and scheduling-related operations used patterns such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_schedules_composite&lt;/span&gt; 
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;schedules&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;week_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;day_of_week&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The most important lesson was that &lt;strong&gt;index order matters&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The leading columns in the index should closely match how the query filters data. Since most queries were tenant-scoped, the tenant identifier typically appeared first. If a query filters on &lt;code&gt;tenant_id&lt;/code&gt; and &lt;code&gt;user_id&lt;/code&gt; but not &lt;code&gt;event_date&lt;/code&gt;, the composite index is still utilized. However, a query filtering only on &lt;code&gt;event_date&lt;/code&gt; would not benefit from this index.&lt;/p&gt;

&lt;p&gt;After reviewing query patterns and introducing composite indexes where appropriate, reporting workloads showed noticeable improvements and several slow queries were eliminated entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Moving Business Logic into Supabase RPC Functions
&lt;/h2&gt;

&lt;p&gt;Initially, many operations required multiple database round trips.&lt;/p&gt;

&lt;p&gt;The application would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch data&lt;/li&gt;
&lt;li&gt;Perform calculations&lt;/li&gt;
&lt;li&gt;Execute additional queries&lt;/li&gt;
&lt;li&gt;Aggregate results&lt;/li&gt;
&lt;li&gt;Return the final response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach worked well during the early stages of development but became increasingly expensive as datasets grew and the latency of consecutive queries compounded.&lt;/p&gt;

&lt;p&gt;To reduce overhead, complex operations were moved into Supabase RPC functions (PostgreSQL PL/pgSQL database functions).&lt;/p&gt;

&lt;p&gt;Instead of multiple client-server interactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
 ├─ Query A (Fetch tenant orders)
 ├─ Query B (Fetch menu metrics)
 ├─ Query C (Insert audit log)
 └─ Processing (Aggregate in Node.js)

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

&lt;/div&gt;



&lt;p&gt;the flow became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
 └─ RPC Function (get_tenant_dashboard_metrics)
      ├─ Validation
      ├─ Aggregation
      ├─ Calculations
      └─ Result

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

&lt;/div&gt;



&lt;p&gt;Here is a simplified example of how we defined the database function in Postgres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;get_tenant_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_tenant_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;total_revenue&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;completed_orders&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt; &lt;span class="k"&gt;DEFINER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;QUERY&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_revenue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;completed_orders&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p_tenant_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;We then call this function in a single, fast client-side request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_tenant_summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;p_tenant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This reduced network round trips, centralized business logic, and improved execution speed. The biggest performance gains were observed in reporting-related workloads where response times dropped from several seconds to millisecond-level execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Simplifying Data Access with Database Views
&lt;/h2&gt;

&lt;p&gt;As the application evolved, certain joins and aggregations began appearing repeatedly throughout the codebase. The same datasets were being reconstructed in multiple places.&lt;/p&gt;

&lt;p&gt;To solve this, database views were introduced.&lt;/p&gt;

&lt;p&gt;For example, to build a consolidated daily activity summary, we defined a Postgres View:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;daily_activity_summary&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;activity_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active_users&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Instead of repeatedly joining numerous tables across different services, the application can now query the view directly, using client-side filters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;daily_activity_summary&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activity_date, total_events, active_users&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tenant_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;activity_date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Benefits included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced query duplication&lt;/li&gt;
&lt;li&gt;Cleaner application code&lt;/li&gt;
&lt;li&gt;Easier reporting development&lt;/li&gt;
&lt;li&gt;Improved maintainability&lt;/li&gt;
&lt;li&gt;Faster onboarding for developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Views effectively became reusable building blocks for reporting and dashboard generation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Eliminating Unnecessary Data Retrieval
&lt;/h2&gt;

&lt;p&gt;During optimization, several endpoints were found to be retrieving significantly more data than required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoiding &lt;code&gt;SELECT *&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;queries were modified to fetch only the required columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;While simple, this optimization reduced network transfer and improved query efficiency across multiple endpoints.&lt;/p&gt;




&lt;h3&gt;
  
  
  Introducing Pagination
&lt;/h3&gt;

&lt;p&gt;Some endpoints attempted to return entire datasets at once. As records increased, response sizes grew unnecessarily.&lt;/p&gt;

&lt;p&gt;Pagination was introduced across reporting and administrative interfaces to ensure only the required records were fetched.&lt;/p&gt;

&lt;p&gt;Instead of returning thousands of rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;queries were limited appropriately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This significantly reduced response sizes and improved perceived application performance.&lt;/p&gt;




&lt;h3&gt;
  
  
  Removing Unnecessary Sorting
&lt;/h3&gt;

&lt;p&gt;Several queries performed sorting operations (&lt;code&gt;ORDER BY&lt;/code&gt;) that were not actually required by the business logic or layout. Removing unnecessary sorting clauses reduced CPU execution time and lowered database workload, especially on larger datasets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optimizing Aggregations
&lt;/h2&gt;

&lt;p&gt;Reporting systems often become slow because aggregation logic is repeatedly recalculated.&lt;/p&gt;

&lt;p&gt;By simplifying query logic, moving heavy aggregations to periodic background rollups (or materialized views), and querying via optimized RPC functions, we reduced database complexity and significantly improved dashboard loading speeds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adopting UUIDv7 for New Tables
&lt;/h2&gt;

&lt;p&gt;As the system evolved, UUIDv7 was adopted for newly created tables.&lt;/p&gt;

&lt;p&gt;Traditional UUIDv4 values are completely random. While they provide excellent uniqueness guarantees, they introduce index fragmentation in B-Tree structures. When a database inserts random values, it forces the B-Tree index to split pages in random locations to fit the keys, lowering write speeds and caching efficiency.&lt;/p&gt;

&lt;p&gt;UUIDv7 solves this by introducing millisecond-level time ordering while maintaining globally unique identifiers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Conceptual structure of UUIDv7:&lt;/span&gt;
&lt;span class="c1"&gt;-- [48-bit Timestamp] [4-bit Version] [12-bit Rand/Seq] [2-bit Variant] [62-bit Random]&lt;/span&gt;

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

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Better insertion locality:&lt;/strong&gt; Since keys are sequential, Postgres inserts new rows at the end of the B-Tree index structure, minimizing page splits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More predictable index growth:&lt;/strong&gt; Better utilization of memory-mapped buffer cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved long-term scalability:&lt;/strong&gt; Higher write throughput on index pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retained uniqueness guarantees:&lt;/strong&gt; No risk of collisions across tenants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To implement this on new tables, we define the column default using a generator:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;/div&gt;



&lt;p&gt;The migration strategy was intentionally conservative: existing tables remained unchanged while newly created tables adopted UUIDv7 moving forward. Since no formal benchmarking was performed on the UUID transition, no specific performance improvements are claimed. However, UUIDv7 aligns well with modern PostgreSQL best practices for large-scale systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Several lessons became clear throughout the optimization process.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Indexes Are Not a Magic Solution
&lt;/h3&gt;

&lt;p&gt;Adding indexes blindly often increases write overhead while providing little benefit. Indexes should be designed around real query patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Composite Indexes Usually Matter More Than Individual Indexes
&lt;/h3&gt;

&lt;p&gt;Understanding how queries filter data is more important than increasing the total number of indexes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Database-Side Processing Can Be Extremely Effective
&lt;/h3&gt;

&lt;p&gt;For aggregation-heavy workloads, RPC functions reduced round trips and simplified application logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Views Improve Maintainability
&lt;/h3&gt;

&lt;p&gt;Complex reporting logic becomes significantly easier to manage when reusable views are introduced.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Pagination Should Be Introduced Early
&lt;/h3&gt;

&lt;p&gt;Large datasets eventually become expensive. Pagination prevents future performance problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Small Optimizations Compound
&lt;/h3&gt;

&lt;p&gt;Removing unnecessary sorting, reducing selected columns, optimizing aggregations, and improving indexes may seem minor individually. Together, they can transform the performance characteristics of a system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Database optimization is rarely about finding a single breakthrough improvement. In this case, the largest gains came from systematically addressing multiple bottlenecks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composite indexing&lt;/li&gt;
&lt;li&gt;RPC-based processing&lt;/li&gt;
&lt;li&gt;Database views&lt;/li&gt;
&lt;li&gt;Query optimization&lt;/li&gt;
&lt;li&gt;Aggregation improvements&lt;/li&gt;
&lt;li&gt;Pagination strategies&lt;/li&gt;
&lt;li&gt;UUIDv7 adoption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was a more maintainable system, faster reporting workflows, and significantly improved responsiveness under production workloads. Most importantly, these improvements were achieved without redesigning the entire architecture. Instead, they came from understanding how the database was being used and optimizing the paths that mattered most.&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>Building an Atomic Delivery Dispatcher with PostgreSQL Row-Level Locking</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/building-an-atomic-delivery-dispatcher-with-postgresql-row-level-locking-3ncf</link>
      <guid>https://dev.to/poojang/building-an-atomic-delivery-dispatcher-with-postgresql-row-level-locking-3ncf</guid>
      <description>&lt;p&gt;Building a logistics and last-mile delivery dispatcher involves more than calculating routes or sending SMS updates. In the real world, you are managing scarce physical resources (riders) operating under strict constraints (weight/capacity limits) in real-time.&lt;/p&gt;

&lt;p&gt;When multiple orders enter the system at peak hours, a logistics backend must guarantee that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Riders are never assigned orders exceeding their physical carrying capacity.&lt;/li&gt;
&lt;li&gt;A rider is never double-booked onto multiple concurrent delivery batches.&lt;/li&gt;
&lt;li&gt;Orders move through a strict, deterministic state progression.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Below is an engineering walkthrough on how we built a concurrency-safe delivery dispatch engine in TypeScript using PostgreSQL row-level locking.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Concurrency Control: Locking with &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When assigning a batch of orders to a rider, the system must check the rider's active load to ensure the additions do not exceed their maximum capacity limit.&lt;/p&gt;

&lt;p&gt;In a standard Node.js environment, querying the database to calculate current load, checking the limits in application memory, and then issuing an &lt;code&gt;UPDATE&lt;/code&gt; leaves a gap for race conditions. If two different order managers attempt to assign batches to the same rider simultaneously, both operations might read the rider's load as "available" before either write completes, resulting in over-allocation.&lt;/p&gt;

&lt;p&gt;To solve this, we use PostgreSQL &lt;strong&gt;row-level locking&lt;/strong&gt; (&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;) inside an explicit transaction block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PoolClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pg&lt;/span&gt;&lt;span class="dl"&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;RiderAssignmentResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assignRiderToBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PoolClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;riderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RiderAssignmentResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Start transaction boundary&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Lock the rider row for modification&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;riderRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT rider_id, availability_status, max_capacity, type 
       FROM delivery_riders 
       WHERE rider_id = $1 
       FOR UPDATE`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;riderId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;riderRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RIDER_NOT_FOUND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;riderRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability_status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AVAILABLE&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RIDER_UNAVAILABLE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Fetch order count in the proposed batch&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batchRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT count(*) as order_count, status FROM delivery_batches WHERE batch_id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;batchId&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;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;batchRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;incomingOrderCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&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;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CREATED&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INVALID_BATCH_STATUS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Calculate rider's current active order load&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activeLoadRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`SELECT COALESCE(SUM(order_count), 0) as current_load 
       FROM delivery_batches 
       WHERE assigned_rider_id = $1 AND status IN ('ASSIGNED', 'PICKED', 'OUT_FOR_DELIVERY')`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;riderId&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;currentLoad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeLoadRes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;current_load&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Validate capacity constraints&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;currentLoad&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;incomingOrderCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max_capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CAPACITY_EXCEEDED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Update batch and assign rider&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE delivery_batches 
       SET assigned_rider_id = $1, status = 'ASSIGNED', assigned_at = NOW() 
       WHERE batch_id = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;riderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 6. Log assignment audit log&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO rider_assignment_history (batch_id, new_rider_id, reason) 
       VALUES ($1, $2, $3)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;riderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;System Auto-Dispatch&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rider assigned successfully&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROLLBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Transaction failed&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Locking Mechanics
&lt;/h3&gt;

&lt;p&gt;When the transaction runs &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on the target rider, Postgres places an exclusive write lock on that specific row. Any other concurrent transaction attempting to read or update that same rider row is forced to queue until the first transaction calls &lt;code&gt;COMMIT&lt;/code&gt; or &lt;code&gt;ROLLBACK&lt;/code&gt;. This guarantees that capacity limits are computed against deterministic, sequential state.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Enforcing State Transitions via State Machines
&lt;/h2&gt;

&lt;p&gt;A delivery batch must follow a strict path to prevent "orphaned" orders or invalid logic (like marked delivered before they are picked up).&lt;/p&gt;

&lt;p&gt;The lifecycle follows this state map:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    [CREATED]
        │
        ▼ (Rider Assigned)
   [ASSIGNED]
        │
        ▼ (Rider Picked Up Order)
    [PICKED]
        │
        ▼ (Transit Started)
[OUT_FOR_DELIVERY]
   ┌────┴────────────────────────┐
   ▼ (Successful Delivery) ▼ (Delivery Failed)
[COMPLETED] [FAILED]

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

&lt;/div&gt;



&lt;p&gt;To enforce this, we build a transition map in TypeScript that acts as our guardrail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BatchStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CREATED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASSIGNED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PICKED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OUT_FOR_DELIVERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMPLETED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CANCELLED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VALID_TRANSITIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BatchStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BatchStatus&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;CREATED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ASSIGNED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CANCELLED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;ASSIGNED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PICKED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CREATED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CANCELLED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// Can reassign back to CREATED&lt;/span&gt;
  &lt;span class="na"&gt;PICKED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OUT_FOR_DELIVERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;OUT_FOR_DELIVERY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMPLETED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;COMPLETED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="c1"&gt;// Terminal State&lt;/span&gt;
  &lt;span class="na"&gt;FAILED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="c1"&gt;// Terminal State&lt;/span&gt;
  &lt;span class="na"&gt;CANCELLED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="c1"&gt;// Terminal State&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateStateTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BatchStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BatchStatus&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VALID_TRANSITIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;current&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;allowed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;By verifying transitions before executing any database changes, the engine blocks invalid states at the API boundaries. For example, if a client tries to patch the status of a &lt;code&gt;CREATED&lt;/code&gt; batch to &lt;code&gt;OUT_FOR_DELIVERY&lt;/code&gt;, the application throws a 400 Bad Request error immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Storing Rich Tracking Data with JSONB Proof of Delivery
&lt;/h2&gt;

&lt;p&gt;When a driver completes a delivery, the app captures rich proof of delivery (PoD): photo URLs (uploaded to S3), coordinate telemetry, customer signatures, notes, and timestamps.&lt;/p&gt;

&lt;p&gt;Instead of writing a rigid relational schema with child tables for tracking metadata, we opted for a Postgres &lt;code&gt;JSONB&lt;/code&gt; column on our batch order mapping table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;delivery_batch_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;batch_order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;batch_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;delivery_batches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;sequence&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'PENDING'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;-- JSONB document holding unstructured proof of delivery&lt;/span&gt;
    &lt;span class="n"&gt;proof_of_delivery&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="n"&gt;completed_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why JSONB instead of structured relational columns?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Metadata:&lt;/strong&gt; Different delivery types require different proofs. A corporate drop-off might require a text note ("Left with receptionist"), while a residential drop-off requires an S3 photo link and GPS bounds. JSONB handles this schema variance without migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced Join Overhead:&lt;/strong&gt; When fetching delivery details for the administration portal, the application reads a single row without executing expensive relational joins to retrieve metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexing Support:&lt;/strong&gt; Postgres allows indexing JSONB fields using GIN (Generalized Inverted Index) keys. If we need to search for deliveries verified within specific coordinates, we can index the JSONB path directly:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_pod_coordinates&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;delivery_batch_orders&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;proof_of_delivery&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'coordinates'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Real-Time Telemetry Updates
&lt;/h2&gt;

&lt;p&gt;Delivery is highly interactive. Customers, outlet merchants, and platform dispatchers need to see location changes and status updates instantly.&lt;/p&gt;

&lt;p&gt;To achieve this, we link our database transactions directly with a real-time event broker (Socket.IO):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;socket.io&lt;/span&gt;&lt;span class="dl"&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;DeliveryEventPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BatchStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outletId&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;riderId&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;updatedAt&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeliveryNotificationService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;broadcastBatchStatus&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="nx"&gt;DeliveryEventPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Notify the merchant outlet dashboard&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`outlet:&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="nx"&gt;outletId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivery_batch_updated&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="c1"&gt;// 2. Notify the specific rider's device&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;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;riderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`rider:&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="nx"&gt;riderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my_batch_updated&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="c1"&gt;// 3. Notify the customer room tracking this order&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`batch:&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="nx"&gt;batchId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tracking_updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This pub/sub routing model ensures that the instant database row updates commit successfully, the web socket layer broadcasts the changes to the respective clients in sub-100ms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary of Dispatcher Core Patterns
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Serialize Resource Allocation:&lt;/strong&gt; Use row locks (&lt;code&gt;FOR UPDATE&lt;/code&gt;) inside isolated database transactions to prevent capacity overloading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defensive State Logic:&lt;/strong&gt; Define a clear, immutable transitions map to enforce linear lifecycle paths on orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage Document Fields for Audits:&lt;/strong&gt; Use &lt;code&gt;JSONB&lt;/code&gt; for storing flexible metadata profiles, keeping reads fast and schema migrations minimal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link DB Events to Websockets:&lt;/strong&gt; Keep clients synchronized by coupling transactional success hooks directly to your real-time notification brokers.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>Designing a Multi-Tenant Subsidy Engine with Concurrency-Safe Budget Tracking</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/designing-a-multi-tenant-subsidy-engine-with-concurrency-safe-budget-tracking-p37</link>
      <guid>https://dev.to/poojang/designing-a-multi-tenant-subsidy-engine-with-concurrency-safe-budget-tracking-p37</guid>
      <description>&lt;p&gt;Expanding a single-tenant business application to support B2B corporate models brings massive complexity to your data access layer. You are no longer just handling independent user checkouts; you are now enforcing complex corporate compliance rules, isolating merchant visibility, managing shared program budgets, and tracking subsidy ledgers.&lt;/p&gt;

&lt;p&gt;As a backend engineer, I recently architected a B2B corporate allowance engine. Below is a deep dive into the core engineering decisions, database design, conflict resolution algorithms, and concurrency patterns required to build a high-integrity multi-tenant corporate wallet.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Multi-Tenant Relational Isolation
&lt;/h2&gt;

&lt;p&gt;In a multi-tenant B2B platform, security starts at the database schema. Corporate organizations contract with the platform to allow their employees to buy meals from a selected pool of merchants.&lt;/p&gt;

&lt;p&gt;To enforce this, we designed a two-tiered outlet mapping structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Platform Level:&lt;/strong&gt; The platform admin assigns a master pool of merchants to the organization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Office Level:&lt;/strong&gt; The corporate admin maps specific merchants from that pool to individual physical offices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is enforced at the database level using composite foreign keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Master Pool assigned to the organization&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;corporate_org_outlets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;organization_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;outlet_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;assigned_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outlet_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Office level assignment mapping to specific offices&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;corporate_office_outlets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;office_outlet_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;office_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;organization_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;outlet_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_visible_in_app&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;-- Enforce that the office outlet MUST exist in the organization's master pool&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outlet_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
        &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;corporate_org_outlets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outlet_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Architectural Benefit
&lt;/h3&gt;

&lt;p&gt;By referencing a composite key &lt;code&gt;(organization_id, outlet_id)&lt;/code&gt; rather than just a single &lt;code&gt;outlet_id&lt;/code&gt;, PostgreSQL prevents a corporate admin from accidentally (or maliciously) displaying an unauthorized merchant to their employees. Data integrity is guaranteed at the database engine level, minimizing reliance on application-layer checks.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Resolving Subsidy Conflicts: The "Maximum Rule"
&lt;/h2&gt;

&lt;p&gt;When an employee checks out, multiple subsidy programs might apply to their order. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Corporate Allowance&lt;/strong&gt; (e.g., 50% subsidy up to $10, eligible for the entire office).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Merchant-Specific Subsidy&lt;/strong&gt; (e.g., a flat $5 discount sponsored for a specific team).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Allowing these programs to stack cumulatively (e.g., applying both) leads to extreme financial leakage where orders can become free or negative-cost.&lt;/p&gt;

&lt;p&gt;To solve this, we implemented the &lt;strong&gt;Maximum Rule&lt;/strong&gt; : evaluate all eligible programs, calculate the absolute discount for each independently, and apply &lt;em&gt;only&lt;/em&gt; the program that grants the user the highest discount.&lt;/p&gt;

&lt;p&gt;Here is a simplified version of the logic executing in the order validation lifecycle:&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;SubsidyProgram&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;programId&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PERCENTAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FIXED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PERCENTAGE_CAPPED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;value&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;maxCap&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderAmount&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="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubsidyProgram&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="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PERCENTAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;orderAmount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PERCENTAGE_CAPPED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;percentageDiscount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orderAmount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percentageDiscount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxCap&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FIXED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;program&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderAmount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBestEligibleSubsidy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;orderAmount&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="nx"&gt;eligiblePrograms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubsidyProgram&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="nl"&gt;programId&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;discountAmount&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eligiblePrograms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;eligiblePrograms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&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;currentDiscount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;current&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;currentDiscount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;discountAmount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;programId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;programId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;discountAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentDiscount&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;best&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;programId&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;discountAmount&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Reduce?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Array.prototype.reduce&lt;/code&gt; pattern executes in linear &lt;code&gt;O(N)&lt;/code&gt; time complexity. Because the list of eligible programs for a single employee is typically small (under 10), this keeps calculation overhead virtually non-existent while guaranteeing deterministic, leak-proof checkouts.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Concurrency-Safe Budget Exhaustion
&lt;/h2&gt;

&lt;p&gt;In corporate subsidy programs, organizations set strict limits (e.g., a total monthly budget of $5,000). Once the budget is exhausted, the program must immediately disable itself, and employees must pay full price.&lt;/p&gt;

&lt;p&gt;In a high-concurrency environment where dozens of users check out at the exact same second, a standard &lt;strong&gt;Select-then-Update&lt;/strong&gt; application-layer loop is highly vulnerable to race conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. User A Reads: budget_used = 4995. Limit = 5000. (Eligible!)
2. User B Reads: budget_used = 4995. Limit = 5000. (Eligible!)
3. User A Updates: budget_used = 4995 + 10 = 5005. (Overdraft!)
4. User B Updates: budget_used = 5005 + 10 = 5015. (Double Overdraft!)

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Solution: Database-Level Atomic Updates
&lt;/h3&gt;

&lt;p&gt;Instead of validating budget state inside Node.js, we offloaded the validation to an &lt;strong&gt;atomic transaction block&lt;/strong&gt; with conditional update locks in PostgreSQL.&lt;/p&gt;

&lt;p&gt;We write a single, atomic SQL statement that increments the budget and checks the boundary condition in one step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Atomic Check-and-Apply Query&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;corporate_subsidies&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;budget_used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;budget_used&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;subsidy_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; 
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;budget_used&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;total_budget&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;subsidy_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;budget_used&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  How the Application Handles It
&lt;/h3&gt;

&lt;p&gt;If the update affects exactly &lt;code&gt;0&lt;/code&gt; rows, the database tells our application that the budget limit has been reached or the program is inactive. Node.js can catch this state and fail the transaction gracefully, prompting the user that the subsidy is no longer available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applySubsidyTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subsidyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;discountAmount&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
    UPDATE corporate_subsidies 
    SET budget_used = budget_used + $1
    WHERE subsidy_id = $2 
      AND is_active = TRUE
      AND (budget_used + $1) &amp;lt;= total_budget
    RETURNING subsidy_id, budget_used;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;discountAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subsidyId&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBSIDY_BUDGET_EXHAUSTED&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;By leveraging PostgreSQL's row locks on update execution, database transactions are serialized cleanly. We completely eliminate race conditions and budget overruns without introducing the latency or complexity of distributed lock managers like Redis Redlock.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Audit Trail Integrity: The Subsidy Ledger
&lt;/h2&gt;

&lt;p&gt;Subsidies are financial transactions. In corporate systems, admins can edit or delete subsidy programs at any time. If you only store references to program IDs, generating historical GST reports, settlement invoices, or user ledgers will yield incorrect values if program parameters have changed.&lt;/p&gt;

&lt;p&gt;To solve this, we built a &lt;strong&gt;Subsidy Ledger&lt;/strong&gt; containing full denormalized snapshots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;corporate_subsidy_ledger&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ledger_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;employee_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subsidy_program_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;applied_amount&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;-- Snapshot data for historical reporting&lt;/span&gt;
    &lt;span class="n"&gt;program_snapshot&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Whenever an order is processed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We calculate the discount using the active program state.&lt;/li&gt;
&lt;li&gt;We record the calculation details and save a stringified snapshot of the parameters (the percentage, cap, limits, and type) directly inside the JSONB column.&lt;/li&gt;
&lt;li&gt;This creates a permanent, immutable ledger entry. Even if an admin updates the program's parameters or deletes the program entirely next month, historical reports will remain 100% accurate and audit-safe.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Summary of Architectural Best Practices
&lt;/h2&gt;

&lt;p&gt;When building financial or B2B entitlement layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Push Integrity to the Database:&lt;/strong&gt; Enforce relational pools via composite foreign keys and composite constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De-stack Early:&lt;/strong&gt; Implement a deterministic resolution algorithm like the Maximum Rule to avoid cumulative discounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce Atomic Bounds:&lt;/strong&gt; Never validate business thresholds in application code. Let SQL updates handle calculations and assertions simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log Immutable State:&lt;/strong&gt; Store point-in-time configuration snapshots alongside transactional tables to maintain audit compliance.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>saas</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>How I Cut AWS Costs by 60% by Fixing Infrastructure Inefficiencies</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/how-i-cut-aws-costs-by-60-by-fixing-infrastructure-inefficiencies-35cg</link>
      <guid>https://dev.to/poojang/how-i-cut-aws-costs-by-60-by-fixing-infrastructure-inefficiencies-35cg</guid>
      <description>&lt;p&gt;When I became the sole backend and infra owner at MealPe, the AWS bill was higher than it needed to be. Not because of over-provisioning - but because of accumulated inefficiencies nobody had cleaned up.&lt;/p&gt;

&lt;p&gt;I didn't redesign the entire system. Instead, I analyzed the bill, audited the runtime patterns, and fixed what was actually broken. Here's exactly how we accomplished a &lt;strong&gt;60% reduction in monthly cloud spend&lt;/strong&gt; while actually improving LCP and request latencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Audit: Where Was the Money Going?
&lt;/h2&gt;

&lt;p&gt;Before making any changes, I analyzed our AWS Billing console line-by-line. The monthly distribution revealed that compute overhead and log retention were dramatically out of proportion with our active user base:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service Category&lt;/th&gt;
&lt;th&gt;Monthly Cost (Before)&lt;/th&gt;
&lt;th&gt;Monthly Cost (After)&lt;/th&gt;
&lt;th&gt;% Reduction&lt;/th&gt;
&lt;th&gt;Primary Culprit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EC2 Compute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$620&lt;/td&gt;
&lt;td&gt;$280&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;54%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blocked event loop, static serving CPU overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3 Storage &amp;amp; Transfer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$380&lt;/td&gt;
&lt;td&gt;$90&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;76%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Indefinite log storage, massive image uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CloudWatch / I/O&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$250&lt;/td&gt;
&lt;td&gt;$110&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Verbose application request/response dumps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1,250&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$480&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;61%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Overall Savings: $770/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. Fixing Excessive Logging Inefficiencies
&lt;/h2&gt;

&lt;p&gt;Our application log files were growing at an unsustainable rate. The Node.js application was generating verbose logs for every inbound request—including full request and response bodies, heavy SQL trace logs, and absolute stack dumps on trivial input errors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Deprecated verbose pattern that flooded CloudWatch and S3&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Large raw JSON buffers&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;These logs were being written to disk and continuously shipped to Amazon S3.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;I audited our log configuration and implemented &lt;strong&gt;Pino&lt;/strong&gt; for structured JSON logging. I restricted verbose request/response logging only to non-production environments and defined clear log levels for our production cluster:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DEBUG/INFO:&lt;/strong&gt; Used only for crucial system boot details and structured API transaction summaries (method, path, status, duration).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WARN/ERROR:&lt;/strong&gt; Reserved for true exception states, database connection alerts, and 500-level codes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This change alone decreased log volume from &lt;strong&gt;~15GB/day to under 800MB/day&lt;/strong&gt; , dramatically dropping both write I/O costs and file shipping overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Setting Up Automated S3 Lifecycle Policies
&lt;/h2&gt;

&lt;p&gt;S3 storage is inexpensive until files accumulate over months without rules. We were storing every daily server log, PDF bill copy, and redundant image backup indefinitely. There was no cleanup process in place.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;I configured strict S3 Lifecycle Rules on our logging and static resource buckets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Daily Server Logs:&lt;/strong&gt; Transition to &lt;strong&gt;S3 Standard-IA (Infrequent Access)&lt;/strong&gt; after 14 days, move to &lt;strong&gt;S3 Glacier Flexible Retrieval&lt;/strong&gt; after 30 days, and permanently expire after 90 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporary File Uploads:&lt;/strong&gt; Automatically expire from the &lt;code&gt;/tmp/&lt;/code&gt; bucket prefix after 7 days.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Implementing these automated lifecycle states trimmed down our total active S3 storage footprint by &lt;strong&gt;over 70%&lt;/strong&gt; in the first billing cycle.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Offloading Static Asset Serving from Node.js to NGINX
&lt;/h2&gt;

&lt;p&gt;This was the most impactful architectural refinement. The Node.js backend process was serving static frontend assets directly. Whenever a client loaded our web app, the request for a Javascript chunk, CSS bundle, SVG logo, or favicon went straight through our Express application.&lt;/p&gt;

&lt;p&gt;Because Node.js is single-threaded, serving static file assets blocks the event loop from doing actual API computations, leading to high CPU usage and queuing lag.&lt;/p&gt;

&lt;h3&gt;
  
  
  The NGINX Solution
&lt;/h3&gt;

&lt;p&gt;I configured &lt;strong&gt;NGINX&lt;/strong&gt; as a reverse proxy in front of Node.js. NGINX is built from the ground up to serve static files with extremely low memory and CPU overhead. By handling static requests at the NGINX layer, we bypass the Node.js event loop completely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         ┌──────────────────────┐
   Web Client ───────▶ │ NGINX Reverse Proxy │
                         └──────────┬───────────┘
                    /static/* │ /api/*
              (CSS, JS, images) │ (REST endpoints)
                    ┌───────────────┴───────────────┐
                    ▼ ▼
        ┌─────────────────────┐ ┌────────────────────────┐
        │ Local Static Files │ │ Node.js / PM2 Cluster │
        └─────────────────────┘ └────────────┬───────────┘
                                                      ▼
                                          ┌────────────────────────┐
                                          │ PostgreSQL Database │
                                          └────────────────────────┘

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configured NGINX Routing Block
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# NGINX config: serve static files directly, proxy only API requests&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;app.mealpe.in&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Static assets served directly by NGINX - never blocks Node.js loop&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/static/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/mealpe-frontend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;30d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;no-transform,&lt;/span&gt; &lt;span class="s"&gt;immutable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# Turn off access logging for static assets to reduce disk I/O&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# API requests proxied to the PM2 Node.js server cluster&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&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;Offloading this work to NGINX decreased our average EC2 instance memory consumption by &lt;strong&gt;35%&lt;/strong&gt; and CPU usage by &lt;strong&gt;40%&lt;/strong&gt;. This allowed us to safely downsize our EC2 compute tier, reducing our monthly server cost significantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;Optimizing infrastructure isn't always about moving to serverless or re-architecting your entire system. Usually, the highest return on investment comes from cleaning up operational inefficiencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logging:&lt;/strong&gt; Keep production logs actionable, structured, and short.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; Enforce S3 lifecycle rules from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of Concerns:&lt;/strong&gt; Let NGINX serve your files, and let Node.js compute your API logic.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>infrastructure</category>
      <category>performance</category>
    </item>
    <item>
      <title>Scaling a Food-Tech SaaS from 1K to 20K Users</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Thu, 01 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/scaling-a-food-tech-saas-from-1k-to-20k-users-3c7k</link>
      <guid>https://dev.to/poojang/scaling-a-food-tech-saas-from-1k-to-20k-users-3c7k</guid>
      <description>&lt;p&gt;When I took over the backend at MealPe, the platform had around 1,000 registered users across a handful of client sites. A year later, it's at 20,000+ users processing 50,000 meals per month. This is what that scaling journey actually looked like - not the clean version, but the real one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Early Cracks
&lt;/h2&gt;

&lt;p&gt;At 1,000 users, most things "worked." But as we onboarded more institutional clients, cracks started showing.&lt;/p&gt;

&lt;p&gt;API response times crept up. Database queries that ran fine with 10,000 rows started timing out at 200,000. Concurrent meal orders during our intense lunch rush (11:30 AM – 1:00 PM) caused request queuing and database locks that slowed the entire site to a crawl.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Database-First Optimization
&lt;/h2&gt;

&lt;p&gt;Almost every scaling problem traced back to the database. When I audited the system, I focused on three immediate interventions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Indexing Strategy Overhaul:&lt;/strong&gt; I audited every query using &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; and added composite indexes for the most common access patterns (such as combining &lt;code&gt;venue_id&lt;/code&gt; with &lt;code&gt;order_status&lt;/code&gt; and &lt;code&gt;delivery_time&lt;/code&gt;). This alone dropped average query time from &lt;strong&gt;800ms to under 50ms&lt;/strong&gt; for the order listing endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema Normalization:&lt;/strong&gt; Some tables had been designed with flexibility in mind (utilizing JSON columns for dynamically defined fields). I migrated these to proper relational columns with appropriate types and foreign key constraints, which dramatically cut down CPU evaluation costs in query filters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection Pooling:&lt;/strong&gt; We were creating new database connections per request, creating heavy TCP handshake overhead. Implementing proper connection pooling using &lt;code&gt;pg-pool&lt;/code&gt; reduced connection overhead and kept connection saturation safely below thresholds.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. API Architecture Refinement
&lt;/h2&gt;

&lt;p&gt;The original API had grown organically - lots of endpoints doing too many things. I refactored our core router with a focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pagination Everywhere:&lt;/strong&gt; Enforced strict query pagination using limit/offset and cursors so that list views never try to render thousands of rows in a single DOM draw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response Shaping:&lt;/strong&gt; Different clients needed different data structures. Instead of sending full database models, I implemented lightweight view DTOs to shrink our network payload size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching Hot Data:&lt;/strong&gt; Venue menus change once a week, not once a second. Adding a cache layer in front of dynamic menus eliminated thousands of database reads per hour.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. The Lunch Rush Problem
&lt;/h2&gt;

&lt;p&gt;Institutional cafeterias have extremely predictable and highly congested usage patterns. Unlike standard food delivery, &lt;strong&gt;80% of all meal orders come in during a tightly bounded 90-minute lunch window&lt;/strong&gt;. This meant our servers needed to handle peak loads that were 15x the daily average.&lt;/p&gt;

&lt;p&gt;To survive these spikes without scaling our cloud costs infinitely:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decoupling Non-Critical Work:&lt;/strong&gt; I offloaded tasks like push notifications, admin email alerts, and analytic log compilations into a background queue processed after peak hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-Computing Availability:&lt;/strong&gt; Instead of scanning ordering records to determine inventory limits on every checkout, we pre-computed meal item quotas every hour, converting an O(N) scan into an O(1) cache read.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Monitoring That Actually Matters
&lt;/h2&gt;

&lt;p&gt;I set up monitoring dashboards that tracked metrics predictive of failures rather than just CPU alerts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API p95 Latency:&lt;/strong&gt; Warns us when p95 responses exceed 200ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection Pool Saturation:&lt;/strong&gt; Alerts us when available database pools drop below 15%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job Queue Depth:&lt;/strong&gt; Helps us spot background processing lag.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently Next Time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan the Indexing Early:&lt;/strong&gt; Invest in database indexes from day one rather than retrofitting them under pressure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bake in Caching First:&lt;/strong&gt; Integrate cache and pagination middlewares into the router framework rather than adding them as custom interventions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realistic Load Testing:&lt;/strong&gt; Build synthetic load tests modeling the concentrated lunch rush early in staging.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>performance</category>
      <category>saas</category>
    </item>
    <item>
      <title>Node.js Performance Optimization: From Slow APIs to Scalable Systems</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/nodejs-performance-optimization-from-slow-apis-to-scalable-systems-53ao</link>
      <guid>https://dev.to/poojang/nodejs-performance-optimization-from-slow-apis-to-scalable-systems-53ao</guid>
      <description>&lt;p&gt;Node.js is incredibly fast out of the box because of its non-blocking I/O model. However, as your transaction volume grows, minor engineering shortcuts turn into devastating performance bottlenecks.&lt;/p&gt;

&lt;p&gt;At MealPe, as the sole backend engineer, I faced the challenge of optimizing our backend to scale from a few dozen concurrent transactions to thousands. Here is a practical, evidence-based guide to identifying Node.js performance bottlenecks and resolving them using production-proven patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Benchmarks: The Impact of Optimization
&lt;/h2&gt;

&lt;p&gt;To demonstrate the concrete impact of these optimizations, I ran load tests using &lt;strong&gt;Autocannon&lt;/strong&gt; on a single t3.medium EC2 instance. The test simulated &lt;strong&gt;100 concurrent users&lt;/strong&gt; making continuous request rounds to a dynamic database endpoint over 10 seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage of Optimization&lt;/th&gt;
&lt;th&gt;Avg Latency (p95)&lt;/th&gt;
&lt;th&gt;Throughput (Req/Sec)&lt;/th&gt;
&lt;th&gt;CPU Utilization&lt;/th&gt;
&lt;th&gt;Error Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Baseline (Single thread, no cache)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;420 ms&lt;/td&gt;
&lt;td&gt;240 req/sec&lt;/td&gt;
&lt;td&gt;95% (bottleneck)&lt;/td&gt;
&lt;td&gt;4.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Step 1: PM2 Clustered (4 Workers)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;110 ms&lt;/td&gt;
&lt;td&gt;880 req/sec&lt;/td&gt;
&lt;td&gt;42% (distributed)&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Step 2: Redis Caching Layer Active&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,200 req/sec&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15% (idle compute)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. Keep the Event Loop Clear (Non-Blocking IO)
&lt;/h2&gt;

&lt;p&gt;Node.js executes JavaScript code in a single-threaded loop. If you block that thread with CPU-intensive tasks—such as image resizing, parsing massive JSON files, or executing synchronous cryptos—every other user request is forced to wait in a queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Deprecated synchronous blocking pattern&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/report&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./large-report.csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Blocks event loop!&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;heavySyncParsing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Blocks event loop!&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processed&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;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;Always use asynchronous, non-blocking file and network operations. If you absolutely must perform CPU-bound tasks, offload them to a separate thread using &lt;strong&gt;Worker Threads&lt;/strong&gt; or queue them in a background job system like &lt;strong&gt;BullMQ&lt;/strong&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Production-ready non-blocking worker pattern&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worker_threads&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/report&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./report-worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;workerData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./large-report.csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processedData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processedData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to compile report&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Leverage Multi-Core Scalability with Clustering
&lt;/h2&gt;

&lt;p&gt;A standard Node.js process runs on a single CPU core. In production environments where your cloud instances have multiple cores, a single thread leaves substantial compute capacity completely idle.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PM2 Cluster Approach
&lt;/h3&gt;

&lt;p&gt;Instead of manually managing this with the native &lt;code&gt;cluster&lt;/code&gt; module, use &lt;strong&gt;PM2 cluster mode&lt;/strong&gt; in production. PM2 handles process lifecycle management and automatic load-balancing between child processes out of the box:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ecosystem.config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apps"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mealpe-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"script"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/server.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"instances"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"max"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Automatically&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;scales&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;available&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;logical&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cores&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"exec_mode"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"env_production"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"NODE_ENV"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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;By deploying in cluster mode, we instantly multiplied our request capacity by &lt;strong&gt;4x&lt;/strong&gt; on our quad-core database proxy server without changing a single line of API routing logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Implement Redis Caching for Read-Heavy Routes
&lt;/h2&gt;

&lt;p&gt;The fastest API request is the one that never touches your primary relational database. Database I/O is almost always the p95 latency bottleneck.&lt;/p&gt;

&lt;p&gt;At MealPe, venue menus and outlet statuses change infrequently but are queried on every single page load. Caching these responses in &lt;strong&gt;Redis&lt;/strong&gt; (an ultra-fast, in-memory store) reduces response times down to sub-10 milliseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Polished API response caching helper&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&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;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCachedMenu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;venueId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&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;cachedData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`menu:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;venueId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedData&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Cache miss - pass to database controller&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MISS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fallback gracefully on cache error - don't crash client&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Query Optimization &amp;amp; Indexing
&lt;/h2&gt;

&lt;p&gt;If your database requests are slow, Node.js will be slow. I audited our queries using &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; in PostgreSQL and addressed the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing Indexes:&lt;/strong&gt; We added indexes on foreign keys and columns used in filtering/sorting (e.g., &lt;code&gt;venue_id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N+1 Query Elimination:&lt;/strong&gt; Replaced recursive database calls inside loops with clean SQL &lt;code&gt;JOIN&lt;/code&gt; statements or Sequelize eager loading (&lt;code&gt;include&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict Pagination:&lt;/strong&gt; Never load whole tables into memory. Enforce strict &lt;code&gt;LIMIT&lt;/code&gt; and &lt;code&gt;OFFSET&lt;/code&gt; (or cursor-based keys) on all listing tables.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Enable Compression
&lt;/h2&gt;

&lt;p&gt;Ensure &lt;strong&gt;Gzip&lt;/strong&gt; or &lt;strong&gt;Brotli&lt;/strong&gt; compression is active on all JSON and text payloads. This reduces payload transfer sizes by up to &lt;strong&gt;70%&lt;/strong&gt; , speeding up rendering times on mobile devices operating on congested cellular networks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compression&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Compresses all outgoing response payloads automatically&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary of Action Items
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Profile Continuously:&lt;/strong&gt; Use &lt;code&gt;clinic.js&lt;/code&gt; or Chrome DevTools to locate memory leaks and CPU blocks under test loads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster by Default:&lt;/strong&gt; Scale to multiple cores using PM2 cluster mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Strategically:&lt;/strong&gt; Cache read-heavy API responses using Redis with realistic TTLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune the Database:&lt;/strong&gt; Set index paths on Postgres and prevent large scans.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>backend</category>
      <category>javascript</category>
      <category>node</category>
      <category>performance</category>
    </item>
    <item>
      <title>Zero-Downtime Deployments with PM2 and NGINX</title>
      <dc:creator>Poojan</dc:creator>
      <pubDate>Sat, 01 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/poojang/zero-downtime-deployments-with-pm2-and-nginx-j8c</link>
      <guid>https://dev.to/poojang/zero-downtime-deployments-with-pm2-and-nginx-j8c</guid>
      <description>&lt;p&gt;When your platform processes meal orders across busy institutional cafeterias, downtime isn't just inconvenient - people don't get fed. High availability is a hard operational requirement, not just a product target.&lt;/p&gt;

&lt;p&gt;The original deployment process was: SSH into the server, &lt;code&gt;git pull&lt;/code&gt;, &lt;code&gt;npm install&lt;/code&gt;, and &lt;code&gt;pm2 restart all&lt;/code&gt;. This caused a &lt;strong&gt;10–30 second outage per deploy&lt;/strong&gt; , during which our API dropped orders and hung terminals.&lt;/p&gt;

&lt;p&gt;Here is exactly how I set up graceful, zero-downtime deployments for our Node.js server cluster using PM2's native clustering and NGINX upstream proxies.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. PM2 Cluster Configuration
&lt;/h2&gt;

&lt;p&gt;The foundation of zero-downtime is simple: never let the number of active app instances drop to zero. PM2's cluster mode makes it straightforward to scale your process across all cores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ecosystem.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mealpe-api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dist/server.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;instances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Scale to all available CPU cores&lt;/span&gt;
    &lt;span class="na"&gt;exec_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_memory_restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;500M&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;listen_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Wait 10s for boot signal&lt;/span&gt;
    &lt;span class="na"&gt;kill_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="c1"&gt;// Wait 5s for clean close&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;instances: 'max'&lt;/code&gt;&lt;/strong&gt; : Spawns a process on each CPU core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;listen_timeout&lt;/code&gt;&lt;/strong&gt; : Instructs PM2 to wait for a database connection or socket handshake before marking a new process as "Online."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kill_timeout&lt;/code&gt;&lt;/strong&gt; : Gives active processes 5 seconds to wrap up in-flight REST queries before forcing a close.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Transitioning to Graceful Reloads
&lt;/h2&gt;

&lt;p&gt;Instead of calling &lt;code&gt;pm2 restart&lt;/code&gt; (which kills all instances simultaneously), we transition to &lt;code&gt;pm2 reload&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reload initiates a &lt;strong&gt;rolling update&lt;/strong&gt; : it spawns a new instance, waits for it to become online, then safely turns down an old instance. It repeats this pattern process-by-process, maintaining maximum API capacity:&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="c"&gt;# Production deploy script (deploy.sh)&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Pulling latest branch code..."&lt;/span&gt;
git pull origin main

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Installing production-only dependencies..."&lt;/span&gt;
npm ci &lt;span class="nt"&gt;--production&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Executing rolling reload..."&lt;/span&gt;
pm2 reload ecosystem.config.js &lt;span class="nt"&gt;--update-env&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deploy successfully completed!"&lt;/span&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Implementing Application Graceful Shutdowns
&lt;/h2&gt;

&lt;p&gt;PM2 sends a &lt;code&gt;SIGINT&lt;/code&gt; trigger to your process before shutting it down. If your application doesn't handle this signal, it terminates instantly, dropping all connections mid-transaction.&lt;/p&gt;

&lt;p&gt;You must catch the &lt;code&gt;SIGINT&lt;/code&gt; event, close the HTTP port to block new inbound traffic, finish active queries, and release database pools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Professional graceful shutdown hook in server.js&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGINT&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGINT signal received. Starting graceful shutdown sequence...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Stop the HTTP server from accepting new socket sessions&lt;/span&gt;
  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP server successfully closed.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Release database connection pools cleanly&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Database pools released. Exiting cleanly.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error during database teardown:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Force close after a 6-second timeout block if connections hang&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forced shutdown active: connections did not close in time.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;6000&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;h2&gt;
  
  
  4. Configuring NGINX Load-Balancing
&lt;/h2&gt;

&lt;p&gt;NGINX is our front-door gateway. The key configurations for zero-downtime routing include setting up an &lt;code&gt;upstream&lt;/code&gt; pool and instructing NGINX to pass traffic to active processes on failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;mealpe_backend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;127.0.0.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# Keep connection channels open to reduce latency&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt; &lt;span class="s"&gt;http2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.mealpe.in&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://mealpe_backend&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# 🚀 Crucial: if one instance is reloading, try next active worker&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_next_upstream&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt; &lt;span class="s"&gt;timeout&lt;/span&gt; &lt;span class="s"&gt;http_502&lt;/span&gt; &lt;span class="s"&gt;http_503&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;15s&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;By adding &lt;code&gt;proxy_next_upstream&lt;/code&gt;, if an instance is in the middle of reloading and drops a connection, NGINX instantly retries the query on a sibling process. The client has zero awareness of the deploy event.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Webhook CI/CD Automation
&lt;/h2&gt;

&lt;p&gt;To secure execution, we set up a lightweight deployment daemon on our EC2 instance that exposes a secured webhook route. When a PR merges into &lt;code&gt;main&lt;/code&gt; on GitHub, our actions pipeline runs tests, builds the typescript bundle, and pings the webhook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions CI deployment step&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Trigger Server Deploy Webhook&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;curl -X POST \&lt;/span&gt;
      &lt;span class="s"&gt;-H "Authorization: Bearer ${{ secrets.DEPLOY_SECRET }}" \&lt;/span&gt;
      &lt;span class="s"&gt;https://api.mealpe.in/webhooks/deploy&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The daemon checks the bearer token (a shared secret) in constant time before running anything. This triggers the automated &lt;code&gt;deploy.sh&lt;/code&gt; script locally on the server.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Outcome
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0 seconds&lt;/strong&gt; of user-facing downtime recorded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;45-second&lt;/strong&gt; deployment pipeline from git merge to production availability.&lt;/li&gt;
&lt;li&gt;Confidence to deploy minor changes and hotfixes safely during standard hours.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>javascript</category>
      <category>node</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
