<?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: Mhamd Ghanoum</title>
    <description>The latest articles on DEV Community by Mhamd Ghanoum (@don21).</description>
    <link>https://dev.to/don21</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%2F3969621%2F141757b2-2f3a-4872-9cc6-1b21878fbbe6.jpg</url>
      <title>DEV Community: Mhamd Ghanoum</title>
      <link>https://dev.to/don21</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/don21"/>
    <language>en</language>
    <item>
      <title>The Reconnect Bug Where Marking a User Online Erased the Data I Needed for Replay</title>
      <dc:creator>Mhamd Ghanoum</dc:creator>
      <pubDate>Thu, 15 Oct 2026 06:58:40 +0000</pubDate>
      <link>https://dev.to/don21/the-reconnect-bug-where-marking-a-user-online-erased-the-data-i-needed-for-replay-4bei</link>
      <guid>https://dev.to/don21/the-reconnect-bug-where-marking-a-user-online-erased-the-data-i-needed-for-replay-4bei</guid>
      <description>&lt;p&gt;While testing a reconnect system for a deliverer-facing WebSocket gateway: when someone reconnects after losing their connection, they should get a snapshot of their current job plus any events they missed while offline. Two pieces, both of which I was confident were correct on their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Marking presence:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;markOnline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isOnline&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;lastSeenAt&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="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;&lt;strong&gt;Computing what was missed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;replayDelivererEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delivererId&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;const&lt;/span&gt; &lt;span class="nx"&gt;deliverer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;delivererProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;delivererId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lastSeenAt&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;span class="na"&gt;activeJobId&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;span class="c1"&gt;// ...fetch events since deliverer.user.lastSeenAt&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;And the connection handler that used both:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handleConnection&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;Socket&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;deliverer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDelivererByUserId&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;sub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markOnline&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;sub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// step 1&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missedEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replayService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replayForDeliverer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deliverer&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;// step 2&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;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;events.replay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;missedEvents&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;I tested it: disconnect, trigger a job assignment while offline, reconnect. &lt;strong&gt;events.replay&lt;/strong&gt; came back empty, every time. The event genuinely happened. The query genuinely should have found it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Walk through the actual timeline. A deliverer disconnects — markOffline correctly stamps lastSeenAt with the disconnect time. Later, they reconnect. handleConnection fires, and the first thing it does is call markOnline, which immediately overwrites lastSeenAt to right now. Only after that does it call replayForDeliverer, which reads lastSeenAt to compute "what happened since they were last seen"  except by then lastSeenAt no longer holds the disconnect time. It holds the current moment, because the line above just wrote over it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;getEventsSince({ since: lastSeenAt })&lt;/strong&gt; effectively became since: right now. The replay feature couldn't fail more quietly — no error, no exception, just a correctly-shaped empty array every single time.&lt;/p&gt;

&lt;p&gt;What makes this bug worth writing about isn't the fix — it's that neither piece of code was wrong in isolation. Marking someone online should update lastSeenAt. Replay should read lastSeenAt. The bug only exists in the order they ran in, and that kind of bug doesn't show up by staring at either function — only by tracing the actual sequence of operations across the whole request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Read before you overwrite:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;handleConnection&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;Socket&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;deliverer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDelivererByUserId&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;sub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Replay FIRST, using the lastSeenAt from before this connection&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missedEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replayService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replayForDeliverer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deliverer&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;// THEN mark online, which overwrites lastSeenAt for next time&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markOnline&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;sub&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="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;events.replay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;missedEvents&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;No schema change needed. Reordering two already-correct operations was the entire fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part worth remembering&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One field, two different jobs — "when did we last hear a heartbeat" and "what's the anchor point for replay" happened to share the same column, and that's exactly what made the collision possible. The one-line reorder fixes today's bug, but it leaves the deeper coupling in place: anything else that later wants to read lastSeenAt for a different purpose can walk into the same trap. The more robust version splits them. a dedicated &lt;strong&gt;lastDisconnectedAt&lt;/strong&gt;, set only on disconnect, read only by replay so two unrelated concerns stop sharing state that one of them mutates as a side effect. I haven't built that yet. It's on the list specifically because I know why it would help, not just that it might.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>postgressql</category>
      <category>prisma</category>
    </item>
    <item>
      <title>Calling a Service Inside prisma.$transaction Doesn't Make It Atomic — Here's the Bug That Taught Me That</title>
      <dc:creator>Mhamd Ghanoum</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:20:27 +0000</pubDate>
      <link>https://dev.to/don21/calling-a-service-inside-prismatransaction-doesnt-make-it-atomic-heres-the-bug-that-taught-me-4jhp</link>
      <guid>https://dev.to/don21/calling-a-service-inside-prismatransaction-doesnt-make-it-atomic-heres-the-bug-that-taught-me-4jhp</guid>
      <description>&lt;p&gt;I was fixing a real bug in a delivery backend I'm building: a customer's order could get created in Postgres even when something downstream failed, leaving an order with no event history behind it. The fix looked simple. Wrap the whole thing in &lt;br&gt;
&lt;strong&gt;prisma.$transaction:&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="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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventLogService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recordEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;entityType&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&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;entityId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EVENTS&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;CREATED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PENDING_VENDOR_CONFIRMATION&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;newOrder&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;I ran it, broke the event-logging step on purpose to test the rollback, and the order still got committed. The transaction did nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$transaction(async (tx) =&amp;gt; {...})&lt;/strong&gt; opens one database connection and starts a transaction on it. The tx object you get inside the callback is bound to that specific connection. Every query run through tx participates in the same transaction.&lt;/p&gt;

&lt;p&gt;My &lt;strong&gt;EventLogService.recordEvent&lt;/strong&gt; method didn't use &lt;strong&gt;tx&lt;/strong&gt;. It used &lt;strong&gt;this.prisma&lt;/strong&gt; — its own injected Prisma client, a completely different connection:&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="nf"&gt;recordEvent&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// not tx — its own connection&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;Nesting the call inside &lt;strong&gt;$transaction&lt;/strong&gt; changes nothing here. Prisma has no way to know "we're inside a transaction right now, please route all queries through it." The only thing that determines whether a query participates in a transaction is which client object actually executed it. Since &lt;strong&gt;recordEvent&lt;/strong&gt; ran on its own connection, its insert committed instantly and independently, with no awareness that anything else was "inside" a transaction around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;&lt;br&gt;
Every method that needs to be transaction-capable has to accept an optional transaction client and fall back to the default when none is passed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;recordEvent&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;tx&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TransactionClient&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt; &lt;span class="o"&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;prisma&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;event&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="nx"&gt;eventLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&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;Then the caller passes its own &lt;strong&gt;tx&lt;/strong&gt; through explicitly:&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventLogService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recordEvent&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;tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Now &lt;strong&gt;recordEvent's&lt;/strong&gt; insert runs on the same connection, inside the same transaction, as everything else. If anything later in that transaction throws, Postgres rolls back the whole connection's uncommitted work — including this insert — because it genuinely was part of the same unit of work, not just nested inside it syntactically.&lt;/p&gt;

&lt;p&gt;I retested the same failure scenario. This time, the order didn't get created either. Rollback worked as a single unit, order and event together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part worth remembering&lt;/strong&gt;&lt;br&gt;
Code structure and transaction membership are two different things. A function call being lexically inside a &lt;strong&gt;$transaction&lt;/strong&gt; block tells you nothing about which database connection it actually used. If you're composing transactional logic across services in Prisma — or any ORM with a similar explicit-client model — every service in that chain needs to accept and thread through the transaction client deliberately. There's no implicit "ambient transaction" you can rely on.&lt;/p&gt;

&lt;p&gt;The honest cost: this makes service signatures a bit noisier — an extra optional parameter on every method that might need to participate in a caller's transaction. I think that's a fair trade for not silently losing atomicity on exactly the kind of multi-step write where you'd assume you had it.&lt;/p&gt;

</description>
      <category>prisma</category>
      <category>postgressql</category>
      <category>backend</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Why I Chose a Simple Capability Token for Waitless — and When I Would Harden It</title>
      <dc:creator>Mhamd Ghanoum</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:24:37 +0000</pubDate>
      <link>https://dev.to/don21/why-i-chose-a-simple-capability-token-for-waitless-and-when-i-would-harden-it-5cf3</link>
      <guid>https://dev.to/don21/why-i-chose-a-simple-capability-token-for-waitless-and-when-i-would-harden-it-5cf3</guid>
      <description>&lt;p&gt;While working on &lt;strong&gt;Waitless&lt;/strong&gt;, I needed customers to be able to track their position in a queue and leave the queue without creating an account.&lt;/p&gt;

&lt;p&gt;That led to a small but interesting security design question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How can I authorize an anonymous user without introducing a full authentication system?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My current solution is deliberately simple: each queue entry receives a random UUID token, and that token is used as a capability for the tracking link.&lt;/p&gt;




&lt;h2&gt;
  
  
  UUID token instead of a sequential ID
&lt;/h2&gt;

&lt;p&gt;A sequential identifier could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/track/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that IDs such as &lt;code&gt;123&lt;/code&gt;, &lt;code&gt;124&lt;/code&gt;, and &lt;code&gt;125&lt;/code&gt; are predictable and enumerable.&lt;/p&gt;

&lt;p&gt;For an anonymous tracking system, I don't want a user to be able to guess another customer's resource simply by changing the ID.&lt;/p&gt;

&lt;p&gt;Instead, an entry has a separate token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Entry {
  id      String @id @default(uuid())
  token   String @unique @default(uuid())

  name     String
  email    String?
  position Int
  status   EntryStatus @default(WAITING)

  queueId String
  // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The public URL therefore looks more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/track/550e8400-e29b-41d4-a716-446655440000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important point is that the token is not intended to be an ordinary identifier.&lt;/p&gt;

&lt;p&gt;It acts as a &lt;strong&gt;capability&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a capability URL?
&lt;/h2&gt;

&lt;p&gt;A capability can be thought of as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;An unguessable secret that grants access to a specific resource.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;possess valid token
        ↓
access this queue entry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The capability is deliberately designed to be difficult to guess.&lt;/p&gt;

&lt;p&gt;That is different from simply hiding a sequential ID and hoping nobody discovers it.&lt;/p&gt;

&lt;p&gt;This distinction matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Security by obscurity
→ "Nobody knows the ID."

Capability-based design
→ "Access requires possession of a deliberately unguessable credential."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is a much more intentional security model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I kept the token simple for now
&lt;/h2&gt;

&lt;p&gt;Waitless is an anonymous queueing system.&lt;/p&gt;

&lt;p&gt;A customer does not need an account, password, or traditional login just to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;see their position&lt;/li&gt;
&lt;li&gt;receive queue updates&lt;/li&gt;
&lt;li&gt;leave the queue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Introducing a complete authentication system would add considerable complexity to the product and to the user experience.&lt;/p&gt;

&lt;p&gt;For the current use case, a capability link gives me a simple model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Join queue
    ↓
Generate random token
    ↓
Give customer tracking link
    ↓
Customer presents token
    ↓
Server identifies the queue entry
    ↓
Allow limited actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a conscious trade-off.&lt;/p&gt;

&lt;p&gt;I'm choosing &lt;strong&gt;simplicity and frictionless anonymous access&lt;/strong&gt; because the current resource is relatively limited in scope.&lt;/p&gt;




&lt;h2&gt;
  
  
  But the token is also a bearer credential
&lt;/h2&gt;

&lt;p&gt;There is an important consequence.&lt;/p&gt;

&lt;p&gt;If possession of the token grants access, then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;whoever possesses the token can use it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That makes the token a bearer credential.&lt;/p&gt;

&lt;p&gt;For example, the current &lt;code&gt;leave()&lt;/code&gt; operation relies on the same tracking capability.&lt;/p&gt;

&lt;p&gt;So if the token is leaked, copied, logged, or shared, another person could potentially use it.&lt;/p&gt;

&lt;p&gt;That means the design is intentionally simple, but it is not the strongest possible security model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Known limitation
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;The tracking token currently also functions as a bearer credential for &lt;code&gt;leave()&lt;/code&gt;, with no rate-limiting or one-time consumption.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I consider this an accepted limitation of the current design rather than something I want to hide.&lt;/p&gt;




&lt;h2&gt;
  
  
  A stronger design: capability URL → session cookie
&lt;/h2&gt;

&lt;p&gt;One way to harden the system would be to use the tracking link only as an initial capability.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/claim/&amp;lt;secret&amp;gt;
      ↓
validate capability
      ↓
create anonymous session
      ↓
HttpOnly + Secure cookie
      ↓
redirect to /track
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the token would no longer need to remain in the URL.&lt;/p&gt;

&lt;p&gt;The browser would use the session cookie for future requests.&lt;/p&gt;

&lt;p&gt;This would reduce some of the risks associated with repeatedly carrying a secret in a URL and would give me more control over session lifecycle, expiration and revocation.&lt;/p&gt;

&lt;p&gt;But it introduces another trade-off.&lt;/p&gt;




&lt;h2&gt;
  
  
  The trade-off: security vs. convenience
&lt;/h2&gt;

&lt;p&gt;The current capability link has an important usability property:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The link itself works across devices.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A customer can open it on their phone, copy it to another device, bookmark it, or reopen it later.&lt;/p&gt;

&lt;p&gt;With a one-time capability-to-session exchange, that behavior becomes more complicated.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phone
  ↓
claim token
  ↓
session created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the same one-time token may no longer be usable on another device.&lt;/p&gt;

&lt;p&gt;Supporting multiple devices would therefore require additional identity/session handling, which increases complexity.&lt;/p&gt;

&lt;p&gt;For a simple anonymous queue, I decided that this trade-off is not currently justified.&lt;/p&gt;




&lt;h1&gt;
  
  
  When would this design change?
&lt;/h1&gt;

&lt;p&gt;The answer is: &lt;strong&gt;when the product starts having identity requirements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine Waitless later introduces a reservation system.&lt;/p&gt;

&lt;p&gt;Now a customer might have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account
   ↓
Reservations
   ↓
Queue entries
   ↓
Payment history
   ↓
Cancellation rules
   ↓
Personal information
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The security requirements become much stronger.&lt;/p&gt;

&lt;p&gt;At that point, a random tracking capability may no longer be enough as the main authorization mechanism.&lt;/p&gt;

&lt;p&gt;I would probably move toward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User authentication
        ↓
Authenticated session
        ↓
Authorization
        ↓
Reservation / queue access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The capability token could still exist for specific use cases, but it would no longer be responsible for the entire identity model.&lt;/p&gt;




&lt;h1&gt;
  
  
  UUIDv4 vs. UUIDv7
&lt;/h1&gt;

&lt;p&gt;This also made me think more carefully about what an identifier is supposed to do.&lt;/p&gt;

&lt;p&gt;For database identifiers, UUIDv7 is attractive because it is time-ordered and therefore provides better index locality than completely random UUIDv4 values.&lt;/p&gt;

&lt;p&gt;For a public capability, however, my primary requirement is different:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I care more about unpredictability than database ordering.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's why I would keep the concepts separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database ID
    ↓
UUIDv7 can be a good choice

Public capability token
    ↓
Cryptographically random token / UUIDv4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an example of a broader engineering principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The best identifier for the database is not necessarily the best credential for the client.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  What about storing the token itself?
&lt;/h1&gt;

&lt;p&gt;Another improvement would be to store only a hash of the capability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raw token
    ↓
SHA-256
    ↓
tokenHash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the database contains the verifier rather than the original bearer secret.&lt;/p&gt;

&lt;p&gt;The application can hash the presented token and compare the result.&lt;/p&gt;

&lt;p&gt;This gives additional protection if the database contents are exposed.&lt;/p&gt;

&lt;p&gt;Again, it adds some implementation complexity, so it is a hardening step rather than something I needed to introduce simply to make the current Waitless flow work.&lt;/p&gt;




&lt;h1&gt;
  
  
  My current decision
&lt;/h1&gt;

&lt;p&gt;For the current version of Waitless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Random capability token
        ↓
Tracking URL
        ↓
Anonymous access
        ↓
Limited actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deliberately chose this because it provides a very small and understandable authorization mechanism without forcing customers through registration and login.&lt;/p&gt;

&lt;p&gt;The trade-off is that the token is a bearer credential, so the system needs to treat it as sensitive.&lt;/p&gt;

&lt;p&gt;I have therefore documented the limitation rather than pretending the design is perfect.&lt;/p&gt;

&lt;p&gt;My planned evolution is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current
Random capability URL
        ↓
Simple anonymous access


Later
Rate limiting
+ expiry / revocation
+ stronger token handling


When identity becomes important
        ↓
Capability URL
        ↓
Session / authentication
        ↓
Fine-grained authorization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting lesson for me was not simply “use UUIDs.”&lt;/p&gt;

&lt;p&gt;It was learning to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is this value actually supposed to represent?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An identifier, a capability, and an authenticated identity are three different concepts.&lt;/p&gt;

&lt;p&gt;Once that distinction is clear, the architecture becomes much easier to reason about.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>security</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>How I Used PostgreSQL Events and MongoDB Event Logs in Two Different Real-World Systems</title>
      <dc:creator>Mhamd Ghanoum</dc:creator>
      <pubDate>Sat, 04 Jul 2026 15:51:22 +0000</pubDate>
      <link>https://dev.to/don21/how-i-used-postgresql-events-and-mongodb-event-logs-in-two-different-real-world-systems-2346</link>
      <guid>https://dev.to/don21/how-i-used-postgresql-events-and-mongodb-event-logs-in-two-different-real-world-systems-2346</guid>
      <description>&lt;p&gt;I built two systems that both needed event tracking.&lt;br&gt;
I used PostgreSQL in one and MongoDB in the other.&lt;br&gt;
Here's exactly why — and what I learned from doing both.&lt;br&gt;
Although the two systems were completely different in purpose, they shared one architectural requirement: each needed a reliable way to record everything that happened — not just the final state, but the story of how that state came to be.&lt;br&gt;
That sounds simple, but the nature of the events in each system was so different that using the same database for both would have caused real problems. This article explains how I approached event tracking in each system, why the solutions diverged, and what the experience taught me about choosing the right persistence model.&lt;/p&gt;
&lt;h2&gt;
  
  
  PostgreSQL Events and Projections in the Delivery Backend
&lt;/h2&gt;

&lt;p&gt;The delivery backend was built around structured workflows: vendors create orders, couriers pick them up, jobs move through well-defined states, and batches group multiple deliveries. Every action in this system has a clear relational meaning. An event like ORDER_CREATED or JOB_STARTED isn't just a log entry — it's part of the domain model.&lt;br&gt;
Because of that, PostgreSQL was the natural home for events. Each event needed to be tied to a specific entity, validated against constraints, indexed for fast lookup, and projected into timeline tables that the dashboard could query efficiently.&lt;br&gt;
The event log table looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model EventLog {
  id          Int             @id @default(autoincrement())
  entityType  EventEntityType
  entityId    Int
  eventType   String
  payload     Json
  createdAt   DateTime        @default(now())

  @@index([entityType, entityId, createdAt])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every event is immutable. Every event belongs to a relational entity. Every event carries structured metadata.&lt;br&gt;
Once an event is written, it is immediately transformed into a timeline entry:&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;switch &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;entityType&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="nx"&gt;EventEntityType&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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderTimelineProjection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;project&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="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nx"&gt;EventEntityType&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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobTimelineProjection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;project&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="k"&gt;break&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 projection layer is what makes the system feel fast. Instead of running complex joins to reconstruct history, the dashboard reads from dedicated timeline tables optimized for sequential queries. It also makes debugging easier: if something goes wrong, I can replay the event stream and see exactly how the system reached its current state.&lt;br&gt;
PostgreSQL excels in this environment because the events are structured, relational, and part of the domain's consistency model. They aren't just analytics — they're part of the truth.&lt;/p&gt;
&lt;h2&gt;
  
  
  MongoDB Event Logs and Aggregation in Waitless
&lt;/h2&gt;

&lt;p&gt;Waitless is a completely different kind of system. It's a real-time queue management platform where customers join via QR code, track their position live, and leave without creating an account. The system generates a lot of events — but these events are not relational. They're behavioral.&lt;br&gt;
A CUSTOMER_JOINED event might contain the customer's position.&lt;br&gt;
A NO_SHOW event might contain a timestamp.&lt;br&gt;
A CUSTOMER_LEFT event might contain a reason.&lt;br&gt;
A CALLED_NEXT event might contain which token was called.&lt;/p&gt;

&lt;p&gt;The shape of the metadata changes depending on the event type. Forcing this into a rigid SQL schema would either require dozens of nullable columns or a JSON column that PostgreSQL would struggle to index efficiently at scale.&lt;br&gt;
MongoDB is built for exactly this. Each event is a document with flexible metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timestamps&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QueueEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Prop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;required&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="nx"&gt;queueId&lt;/span&gt;&lt;span class="o"&gt;!&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="nd"&gt;Prop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;required&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;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueueEventType&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;eventType&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QueueEventType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Prop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="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="nd"&gt;Prop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;metadata&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flexibility becomes powerful when combined with MongoDB's aggregation pipeline. Waitless computes analytics like total joined, total arrived, total no-shows, completion rate, and drop-off rate — all derived from event counts, not relational queries.&lt;br&gt;
A note on Redis: Waitless already had Redis in the stack backing BullMQ. But Redis is a key-value store optimized for job queues and caching — not for querying across documents or running aggregations. MongoDB was the right fit for this specific need.&lt;br&gt;
The separation between MongoDB and PostgreSQL is also crucial for performance. If analytics were computed in PostgreSQL, every dashboard refresh would compete with real-time queue operations for the same connection pool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If I had used PostgreSQL for Waitless events, every analytics query would hit the same connection pool as the queue state — meaning a slow dashboard query could delay a customer joining a queue.&lt;br&gt;
MongoDB avoids this entirely. It handles flexible event shapes, high write throughput, and analytics workloads without interfering with the transactional state stored in PostgreSQL.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL for One and MongoDB for the Other?
&lt;/h2&gt;

&lt;p&gt;The answer becomes clear once you see the nature of the data.&lt;br&gt;
In the delivery backend, events are part of the domain model. They describe state transitions, belong to relational entities, and require strong consistency. PostgreSQL is the right tool.&lt;br&gt;
In Waitless, events are part of analytics. They are flexible, metadata-heavy, and high-volume. They have no relational meaning — they're behavioral observations about what happened in a queue session. MongoDB is the right tool.&lt;br&gt;
Using PostgreSQL for both would have created unnecessary coupling and performance bottlenecks. Using MongoDB for both would have sacrificed relational guarantees where they mattered most.&lt;br&gt;
This is the essence of polyglot persistence: choose the database that matches the shape and purpose of the data, not the one you're most comfortable with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question That Actually Matters
&lt;/h2&gt;

&lt;p&gt;Building two event-driven systems taught me that "SQL vs NoSQL" is the wrong question entirely.&lt;br&gt;
The real question is: what role do your events play in the system?&lt;br&gt;
If events belong to the domain model — use a relational database.&lt;br&gt;
If events belong to analytics — use a document database.&lt;br&gt;
PostgreSQL gave me structured timelines and strong consistency. MongoDB gave me flexible event logs and fast aggregation. Both systems use events, but the events serve completely different masters.&lt;br&gt;
Understanding that difference before writing a single line of code is what separates a working system from a well-designed one.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>postgres</category>
      <category>nestjs</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Understanding SQL Transactions: ACID, Isolation Levels, and How to Handle Locks</title>
      <dc:creator>Mhamd Ghanoum</dc:creator>
      <pubDate>Sun, 28 Jun 2026 10:57:12 +0000</pubDate>
      <link>https://dev.to/don21/understanding-sql-transactions-acid-isolation-levels-and-how-to-handle-locks-24g3</link>
      <guid>https://dev.to/don21/understanding-sql-transactions-acid-isolation-levels-and-how-to-handle-locks-24g3</guid>
      <description>&lt;p&gt;Modern backend systems rely on transactions to guarantee correctness especially in financial operations, delivery platforms, and any workflow where multiple users modify shared data.&lt;br&gt;
A solid understanding of ACID, isolation levels, and locking is essential for designing reliable systems and performing well in backend interviews.&lt;/p&gt;

&lt;p&gt;This article explains how transactions work, how isolation is implemented, and how to handle locks in real SQL databases such as PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. What Is a Transaction?&lt;/strong&gt;&lt;br&gt;
A Transaction is a group of SQL operations that must be executed as a single atomic unit.&lt;br&gt;
Either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;all operations succeed → COMMIT, or&lt;/li&gt;
&lt;li&gt;all operations fail → ROLLBACK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example: Paying for a pizza&lt;/strong&gt;&lt;br&gt;
When customer clicks Buy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deduct $20 from their balance&lt;/li&gt;
&lt;li&gt;Transfer $20 to the restaurant&lt;/li&gt;
&lt;li&gt;Create the order&lt;/li&gt;
&lt;li&gt;Commit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the server crashes after step 1, the database rolls back the deduction.&lt;br&gt;
This prevents partial updates and ensures data correctness.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;2. ACID Properties&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A — Atomicity&lt;br&gt;
All steps succeed or none do.&lt;/p&gt;

&lt;p&gt;C — Consistency&lt;br&gt;
The database always moves from one valid state to another.&lt;/p&gt;

&lt;p&gt;I — Isolation&lt;br&gt;
Each transaction behaves as if it’s the only one running.&lt;/p&gt;

&lt;p&gt;D — Durability&lt;br&gt;
Once committed, data survives crashes.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;3. Isolation Problems&lt;/strong&gt;&lt;br&gt;
When multiple transactions run concurrently, anomalies can occur:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dirty Read — reading uncommitted data&lt;/li&gt;
&lt;li&gt;Non‑repeatable Read — reading the same row twice with different results&lt;/li&gt;
&lt;li&gt;Phantom Read — new rows appear between identical queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Isolation levels define which anomalies are allowed.&lt;/p&gt;

&lt;p&gt;PostgreSQL uses MVCC (Multi‑Version Concurrency Control), which allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;readers to avoid blocking writers&lt;/li&gt;
&lt;li&gt;writers to avoid blocking readers&lt;/li&gt;
&lt;li&gt;This improves performance and reduces unnecessary locking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. How to Deal with Isolation in SQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Isolation is handled by choosing the appropriate isolation level for the workload.&lt;br&gt;
For high‑throughput systems, Read Committed is usually enough.&lt;br&gt;
For operations requiring strict correctness, Repeatable Read or Serializable is used.&lt;br&gt;
PostgreSQL relies on MVCC snapshots to avoid unnecessary locking, and optimistic concurrency (versioning) can be used to prevent lost updates without blocking other transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Isolation Levels&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read Uncommitted: all violations allowed.&lt;/li&gt;
&lt;li&gt;Read Committed: prevents dirty reads only.&lt;/li&gt;
&lt;li&gt;Repeatable Read: prevents dirty reads and Non‑repeatable Read.&lt;/li&gt;
&lt;li&gt;Serializable: prevents all violations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. How to Deal with Locks in SQL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Locks are handled by using them intentionally and keeping them as short as possible.&lt;br&gt;
When exclusive access is required for example, assigning a driver to an order — SELECT … FOR UPDATE is used to lock the row.&lt;br&gt;
Long‑running transactions are avoided, optimistic locking is used to reduce contention, and deadlock errors are caught and retried.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;br&gt;
Two drivers attempt to claim the same delivery order simultaneously.&lt;/p&gt;

&lt;p&gt;Step 1 — Lock the row&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This places a row‑level exclusive lock.&lt;br&gt;
Other transactions attempting to lock or update this row will wait.&lt;/p&gt;

&lt;p&gt;Step 2 — Update safely&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;driver_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&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;'assigned'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the row is locked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no two drivers can claim the same order&lt;/li&gt;
&lt;li&gt;no race conditions occur&lt;/li&gt;
&lt;li&gt;no lost updates happen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the lock is released.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Preventing Lost Updates&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three common strategies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Atomic Update
&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;SET&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;'done'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;AND&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Optimistic Locking (Versioning)
&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;SET&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;'done'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="o"&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;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Explicit Locking
&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;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;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Deadlocks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A deadlock occurs when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transaction A waits for B&lt;/li&gt;
&lt;li&gt;Transaction B waits for A&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL resolves this automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one transaction is terminated&lt;/li&gt;
&lt;li&gt;an error is returned&lt;/li&gt;
&lt;li&gt;the application retries the operation
Deadlocks are normal in high‑concurrency systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Transactions are the foundation of reliable backend systems.&lt;br&gt;
A strong understanding of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ACID&lt;/li&gt;
&lt;li&gt;isolation levels&lt;/li&gt;
&lt;li&gt;MVCC&lt;/li&gt;
&lt;li&gt;locking&lt;/li&gt;
&lt;li&gt;deadlocks&lt;/li&gt;
&lt;li&gt;optimistic concurrency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…is essential for building correct, scalable, and safe applications.&lt;/p&gt;

&lt;p&gt;This knowledge is frequently tested in backend interviews, especially for roles involving distributed systems, financial operations, or high‑concurrency workloads.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>acid</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Indexing &amp; Query Optimization: How to Make Your Database Fast</title>
      <dc:creator>Mhamd Ghanoum</dc:creator>
      <pubDate>Tue, 09 Jun 2026 17:14:40 +0000</pubDate>
      <link>https://dev.to/don21/indexing-query-optimization-how-to-make-your-database-fast-5elp</link>
      <guid>https://dev.to/don21/indexing-query-optimization-how-to-make-your-database-fast-5elp</guid>
      <description>&lt;p&gt;Imagine you have a huge vendors table in an e‑commerce system, and thousands of orders are created every second.&lt;br&gt;
Every time a new order comes in, the system needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;check if the vendor exists&lt;/li&gt;
&lt;li&gt;check if the vendor is active&lt;/li&gt;
&lt;li&gt;check if the product belongs to that vendor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your table is large, the database will scan the entire table to find the vendor.&lt;br&gt;
This is called a full table scan, and it becomes extremely slow as your data grows.&lt;/p&gt;

&lt;p&gt;This is where indexes save your system.&lt;/p&gt;

&lt;p&gt;🟦 What Is an Index?&lt;br&gt;
An index is a small, optimized data structure that tells the database engine exactly where a specific value is located.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instead of scanning the whole table, the database:&lt;/li&gt;
&lt;li&gt;looks at the index&lt;/li&gt;
&lt;li&gt;finds the exact row location&lt;/li&gt;
&lt;li&gt;jumps directly to it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns a slow O(n) search into a fast O(log n) lookup.&lt;/p&gt;

&lt;p&gt;🟩 Example: Indexing the Vendor ID&lt;br&gt;
Without an index:&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;vendors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database scans the entire table.&lt;/p&gt;

&lt;p&gt;With an index:&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_vendors_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;vendors&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checks the index&lt;/li&gt;
&lt;li&gt;finds the row instantly&lt;/li&gt;
&lt;li&gt;returns the result in milliseconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the difference between a system that collapses under load…&lt;br&gt;
and a system that handles millions of requests smoothly.&lt;/p&gt;

&lt;p&gt;🟧 Why Not Index Everything? (The Trade‑Off)&lt;br&gt;
Indexes make reads faster,&lt;br&gt;
but they make writes slower.&lt;/p&gt;

&lt;p&gt;Every time you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;INSERT&lt;/li&gt;
&lt;li&gt;UPDATE&lt;/li&gt;
&lt;li&gt;DELETE
…the database must also update every index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Too many indexes = slow writes = bottlenecks.&lt;/p&gt;

&lt;p&gt;So we only index the hot paths — the most frequently queried fields.&lt;/p&gt;

&lt;p&gt;🟪 Clustered vs Non‑Clustered Indexes (PostgreSQL)&lt;br&gt;
Clustered Index&lt;br&gt;
The table is physically sorted by the index&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast range queries&lt;/li&gt;
&lt;li&gt;Only one per table&lt;/li&gt;
&lt;li&gt;Non‑Clustered Index&lt;/li&gt;
&lt;li&gt;Separate structure&lt;/li&gt;
&lt;li&gt;Points to the actual rows&lt;/li&gt;
&lt;li&gt;You can have many of them&lt;/li&gt;
&lt;li&gt;PostgreSQL uses B‑Tree indexes by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🟦 Other Index Types in PostgreSQL (Short Overview)&lt;br&gt;
PostgreSQL supports several index types optimized for different use cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B‑Tree (default)&lt;/strong&gt;&lt;br&gt;
Best for equality and range queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hash Index&lt;/strong&gt;&lt;br&gt;
Fast equality lookups (=), but limited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GIN Index&lt;/strong&gt;&lt;br&gt;
Perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSONB&lt;/li&gt;
&lt;li&gt;Arrays&lt;/li&gt;
&lt;li&gt;Full‑text search&lt;/li&gt;
&lt;li&gt;Tags&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&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_products_tags&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&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;tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GiST Index&lt;/strong&gt;&lt;br&gt;
Used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Geospatial data&lt;/li&gt;
&lt;li&gt;Distances&lt;/li&gt;
&lt;li&gt;Geometric shapes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example (useful in delivery apps):&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_locations_gist&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;locations&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;gist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;geo_point&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;BRIN Index&lt;/strong&gt;&lt;br&gt;
Great for very large tables with naturally ordered data (logs, events, time‑series).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial Index&lt;/strong&gt;&lt;br&gt;
Index with a condition:&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_active_vendors&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;vendors&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expression Index&lt;/strong&gt;&lt;br&gt;
Index on computed values:&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_lower_email&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flexibility is one of PostgreSQL’s biggest strengths.&lt;/p&gt;

&lt;p&gt;🟨 &lt;strong&gt;Query Optimization Tips&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Indexing is powerful, but you also need efficient queries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;✔️ Select only the columns you need&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;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vendors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&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;vendors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔️ Use wildcards only at the end&lt;br&gt;
Good:&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'Sam%'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bad:&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%Sam%'&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;✔️ Use LIMIT when previewing data&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;orders&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✔️ Run heavy queries during off‑peak hours&lt;br&gt;
Especially analytics or batch jobs.&lt;/p&gt;

&lt;p&gt;🟫 Conclusion&lt;br&gt;
Indexes make your reads extremely fast&lt;/p&gt;

&lt;p&gt;But they slow down writes&lt;/p&gt;

&lt;p&gt;Use them wisely on the most important fields&lt;/p&gt;

&lt;p&gt;Combine indexing with good query practices&lt;/p&gt;

&lt;p&gt;Always measure performance before and after&lt;/p&gt;

&lt;p&gt;Indexing is one of the simplest ways to make your backend feel instant, even under heavy load&lt;/p&gt;

</description>
      <category>database</category>
      <category>indexing</category>
      <category>postgres</category>
    </item>
    <item>
      <title>SQL vs NoSQL — How to Choose the Right Database for Your System</title>
      <dc:creator>Mhamd Ghanoum</dc:creator>
      <pubDate>Fri, 05 Jun 2026 10:32:01 +0000</pubDate>
      <link>https://dev.to/don21/sql-vs-nosql-how-to-choose-the-right-database-for-your-system-2iic</link>
      <guid>https://dev.to/don21/sql-vs-nosql-how-to-choose-the-right-database-for-your-system-2iic</guid>
      <description>&lt;p&gt;Hello everyone!&lt;br&gt;
My name is Mhamd, a backend engineer who loves building scalable systems and explaining complex backend topics in a simple, practical way.&lt;/p&gt;

&lt;p&gt;If you enjoy system design, backend architecture, or want to exchange ideas — feel free to reach out!&lt;/p&gt;

&lt;p&gt;Why This Topic Matters?&lt;br&gt;
When designing a new system, one of the most important architectural decisions is choosing the right type of database.&lt;/p&gt;

&lt;p&gt;This choice affects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;performance&lt;/li&gt;
&lt;li&gt;scalability&lt;/li&gt;
&lt;li&gt;consistency&lt;/li&gt;
&lt;li&gt;development speed&lt;/li&gt;
&lt;li&gt;and even cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I’ll break down SQL vs NoSQL, when to use each, and how I combine both in real-world projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL — Structured, Relational, and Consistent&lt;/strong&gt;&lt;br&gt;
 SQL databases (PostgreSQL, MySQL, SQL Server…) rely on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A predefined schema&lt;/li&gt;
&lt;li&gt;Strong consistency&lt;/li&gt;
&lt;li&gt;ACID transactions&lt;/li&gt;
&lt;li&gt;Clear relationships between tables
When SQL is a great fit?
Use SQL when:&lt;/li&gt;
&lt;li&gt;Your data structure is known in advance&lt;/li&gt;
&lt;li&gt;You need strong consistency&lt;/li&gt;
&lt;li&gt;You have complex relationships (Users ↔ Orders ↔ Products)&lt;/li&gt;
&lt;li&gt;You need powerful queries and joins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: E‑commerce&lt;br&gt;
Entities like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Products&lt;/li&gt;
&lt;li&gt;Users&lt;/li&gt;
&lt;li&gt;Orders&lt;/li&gt;
&lt;li&gt;Payments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…have clear relationships and require consistent transactions.&lt;br&gt;
SQL is perfect here.&lt;/p&gt;

&lt;p&gt;But what about Amazon‑scale systems?&lt;br&gt;
As the system grows globally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;traffic explodes&lt;/li&gt;
&lt;li&gt;data becomes distributed&lt;/li&gt;
&lt;li&gt;sharding becomes complex&lt;/li&gt;
&lt;li&gt;consistency becomes harder&lt;/li&gt;
&lt;li&gt;SQL still works — but requires advanced architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;NoSQL — Flexible, Fast, and Horizontally Scalable&lt;/strong&gt;&lt;br&gt;
NoSQL is not one technology — it’s a family:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document stores (MongoDB)&lt;/li&gt;
&lt;li&gt;Key‑value stores (Redis)&lt;/li&gt;
&lt;li&gt;Wide‑column stores (Cassandra)&lt;/li&gt;
&lt;li&gt;Graph databases (Neo4j)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When NoSQL is a great fit?&lt;br&gt;
Use NoSQL when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your data is semi‑structured or unstructured&lt;/li&gt;
&lt;li&gt;You need high write throughput&lt;/li&gt;
&lt;li&gt;You need horizontal scaling&lt;/li&gt;
&lt;li&gt;You’re building real‑time systems&lt;/li&gt;
&lt;li&gt;Your schema changes frequently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;li&gt;Real‑time feeds&lt;/li&gt;
&lt;li&gt;Chat systems&lt;/li&gt;
&lt;li&gt;IoT&lt;/li&gt;
&lt;li&gt;Caching layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Using Both Together — The Hybrid Approach&lt;br&gt;
Modern systems rarely fit into one category&lt;/strong&gt;.&lt;br&gt;
In my projects, I often combine SQL + NoSQL.&lt;/p&gt;

&lt;p&gt;Example: My Delivery Backend System&lt;br&gt;
PostgreSQL for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orders&lt;/li&gt;
&lt;li&gt;Products&lt;/li&gt;
&lt;li&gt;Users&lt;/li&gt;
&lt;li&gt;State machines&lt;/li&gt;
&lt;li&gt;Auditing&lt;/li&gt;
&lt;li&gt;Because these require structure and consistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redis for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real‑time deliverer tracking&lt;/li&gt;
&lt;li&gt;WebSocket events&lt;/li&gt;
&lt;li&gt;Caching&lt;/li&gt;
&lt;li&gt;Fast lookups&lt;/li&gt;
&lt;li&gt;Because these require speed and flexibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What Happens When You Use GraphQL on Top of PostgreSQL?&lt;/p&gt;

&lt;p&gt;When you put GraphQL above PostgreSQL:&lt;/p&gt;

&lt;p&gt;PostgreSQL remains the source of truth GraphQL becomes a flexible API layer The client can request exactly the data it needs No overfetching or underfetching.Perfect for dashboards and mobile apps&lt;/p&gt;

&lt;p&gt;This combination gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL consistency&lt;/li&gt;
&lt;li&gt;GraphQL flexibility&lt;/li&gt;
&lt;li&gt;A single endpoint for all queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;SQL is perfect for structured, relational, consistent data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NoSQL is perfect for speed, flexibility, and large-scale distributed systems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most real systems use both.&lt;/strong&gt;&lt;br&gt;
The right choice depends on your data and your system’s behavior — not on trends.&lt;/p&gt;

&lt;p&gt;If you enjoyed this article&lt;br&gt;
I’ll be writing more about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system design&lt;/li&gt;
&lt;li&gt;caching&lt;/li&gt;
&lt;li&gt;event-driven architecture&lt;/li&gt;
&lt;li&gt;microservices&lt;/li&gt;
&lt;li&gt;real-time backend patter
ns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Follow me on DEV Community — and feel free to share your thoughts or questions!&lt;/p&gt;

</description>
      <category>sql</category>
      <category>nosql</category>
      <category>postgres</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
