<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Jatin Jain Saraf — Engineering Blog</title>
    <link>https://insight.jatinjainsaraf.com</link>
    <atom:link href="https://insight.jatinjainsaraf.com/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Deep-dive technical articles on PostgreSQL, blockchain infrastructure, database optimization, and system design — from production at Supra Blockchain.</description>
    <language>en-us</language>
    <lastBuildDate>Mon, 14 Sep 2026 05:05:17 GMT</lastBuildDate>
    <managingEditor>j.saraf@supra.com (Jatin Jain Saraf)</managingEditor>
    <webMaster>j.saraf@supra.com (Jatin Jain Saraf)</webMaster>
    <image>
      <url>https://insight.jatinjainsaraf.com/api/og/blog?v=2026-10-03b</url>
      <title>Jatin Jain Saraf — Engineering Blog</title>
      <link>https://insight.jatinjainsaraf.com</link>
    </image>
    <ttl>60</ttl>
    <item>
      <title>Every Postgres Write Triggers Five Background Processes. Here&apos;s What Each One Actually Does.</title>
      <link>https://insight.jatinjainsaraf.com/postgres-wal-checkpoints-wait-events-vacuum-analyze-explained</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/postgres-wal-checkpoints-wait-events-vacuum-analyze-explained</guid>
      <description>An UPDATE returns in two milliseconds, but underneath, Postgres just ran WAL logging, checkpoint bookkeeping, wait-event tracking, vacuum, and ANALYZE to make that write durable, recoverable, and still fast to plan. Here&apos;s what each of those five processes actually does, and which one is usually behind the latency spike that has no slow query in sight.</description>
      <content:encoded><![CDATA[<p>An <code>UPDATE</code> statement returns in two milliseconds. From where you're sitting, that's the whole story: the row changed, the client got an acknowledgment, done. Underneath, Postgres just did five separate jobs to make that write durable, recoverable, reusable, and still fast for the next query to plan correctly. Most of the time you never see any of them. Then a dashboard shows periodic latency spikes with no slow query in sight, or <code>pg_stat_activity</code> shows a backend "stuck" on something with a name you've never had to learn, and you're suddenly debugging a part of Postgres that has been running the entire time.</p>
<p>This is a walkthrough of that background layer: the Write-Ahead Log, checkpoints, the wait events that make both of them visible, and the vacuum/analyze cycle that cleans up after every write and keeps the query planner honest.</p>
<h2>WAL: the black box that makes crash recovery possible</h2>
<p>Postgres never writes a change directly to a table's data file first. It writes a record of the change to the Write-Ahead Log (WAL) first, and only later, asynchronously, does the actual data file get updated. This is the same idea as an aircraft's flight data recorder: it doesn't prevent anything from going wrong, but if the plane goes down, the black box is what lets you reconstruct exactly what happened, in order, right up to the last recorded moment.</p>
<p>If Postgres crashes between "WAL record written" and "data file updated," the data file is allowed to be behind. On restart, Postgres replays the WAL from the last confirmed checkpoint forward and reconstructs every change that was durably logged but not yet applied to the heap. That's the entire crash-recovery guarantee, and it's also the entire replication guarantee: a replica is, at its core, a process continuously replaying WAL records shipped from the primary.</p>
<p><strong>Why this matters in production:</strong> <code>synchronous_commit</code> controls whether a transaction waits for its WAL record to actually hit disk before returning success to the client. Turn it off (or set it to a weaker level like <code>local</code>) and writes get faster, because you're no longer waiting on an fsync. You're also now willing to lose the last few transactions if the server crashes before that WAL hits disk. That's a real trade-off teams make deliberately for high-throughput, loss-tolerant write paths, and a real trade-off teams make by accident when they copy a config from a benchmark blog post.</p>
<h2>Checkpoints: the point WAL gets allowed to forget the past</h2>
<p>WAL can't grow forever, and replaying an unbounded WAL after a crash would make recovery take unbounded time. A checkpoint is Postgres periodically flushing every dirty page sitting in shared buffers out to the actual data files, so that everything the WAL described up to that point is now durably reflected on disk. Once a checkpoint completes, the WAL segments before it are no longer needed for crash recovery and can be recycled or removed (subject to what replication and archiving still need them for).</p>
<p>Checkpoints run on a schedule controlled by two settings: <code>checkpoint_timeout</code> (time-based, five minutes by default) and <code>max_wal_size</code> (a soft cap on how much WAL can accumulate before a checkpoint is forced early). Whichever threshold is hit first triggers the checkpoint.</p>
<p><strong>Why this matters in production:</strong> a checkpoint means writing out every dirty page at once, which is a burst of I/O. If that burst happens all at once, you get the classic "checkpoint spike," periodic latency jumps that correlate with wall-clock time, not query volume. <code>checkpoint_completion_target</code> exists specifically to spread that I/O out over the checkpoint interval instead of dumping it all at once. If your latency graphs show a sawtooth pattern with a period matching your <code>checkpoint_timeout</code>, that's usually the first setting worth checking, not the query itself.</p>
<h2>Wait events: the window into both of the above</h2>
<p><code>pg_stat_activity</code> has two columns, <code>wait_event_type</code> and <code>wait_event</code>, that tell you what a backend is actually blocked on right now, if it's blocked on anything. This is the diagnostic surface that turns "the query is slow" into "the query is waiting on something specific":</p>
<pre><code class="language-sql">SELECT pid, wait_event_type, wait_event, state, query
FROM pg_stat_activity
WHERE wait_event IS NOT NULL;
</code></pre>
<p>The <code>wait_event_type</code> column groups events into categories: <code>Lock</code> (waiting on a row, table, or advisory lock held by another transaction), <code>LWLock</code> (waiting on an internal lightweight lock protecting shared memory structures), <code>IO</code> (waiting on an actual disk read or write), <code>BufferPin</code>, <code>IPC</code>, <code>Timeout</code>, and a few others. A backend showing <code>IO</code> wait events tied to WAL activity, or an <code>LWLock</code> wait tied to the WAL insert path, is a backend that's been caught behind the exact mechanism described above: it's trying to get its change durably logged and something (usually disk throughput, sometimes a checkpoint mid-flush) is making it wait its turn.</p>
<p><strong>Why this matters in production:</strong> the specific wait event names attached to WAL and checkpoint activity have changed across major Postgres versions as the I/O statistics system was reworked, so don't memorize a table of exact strings from a blog post (including this one) and assume it matches your version. Instead, learn the query above, run it against your own instance during a slow period, and read whatever <code>wait_event_type</code>/<code>wait_event</code> pair comes back against the current docs for your version. That habit outlives any specific release.</p>
<h2>Vacuum: cleaning up after MVCC, not just reclaiming disk</h2>
<p>Postgres never overwrites a row in place on <code>UPDATE</code> or <code>DELETE</code>. MVCC (multi-version concurrency control) means an <code>UPDATE</code> writes a new row version and marks the old one dead, and a <code>DELETE</code> just marks a row dead, because other transactions that started before yours might still need to see the old version. Those dead row versions don't disappear on their own. <code>VACUUM</code> is the process that finds them, confirms no transaction still needs them, and marks that space reusable by future inserts on the same table.</p>
<p>Plain <code>VACUUM</code> does not shrink the file on disk (only <code>VACUUM FULL</code> does, by rewriting the whole table, which takes an exclusive lock). It reclaims space <em>within</em> the existing file for future rows to reuse. <code>autovacuum</code> runs this automatically, table by table, once the number of dead tuples crosses a threshold (<code>autovacuum_vacuum_threshold</code> plus a percentage of the table's row count, <code>autovacuum_vacuum_scale_factor</code>, which defaults to 20%). Vacuum also has a second job unrelated to space at all: it's what prevents transaction ID wraparound, a much rarer but far more serious failure mode where an un-vacuumed table's transaction ID counter runs out of room entirely.</p>
<p><strong>Why this matters in production:</strong> vacuum generates its own WAL records, since it's modifying pages, so a large autovacuum run on a big table shows up as real write volume, not a free background chore. It also competes for the same I/O bandwidth a checkpoint is using, which is why an aggressive autovacuum kicking off right as a checkpoint is flushing dirty pages is a common, specific cause of a latency spike that doesn't correlate with any query change. <a href="/blog/database-efficiency-101-understanding-bloat-vacuum-and-the-power-of-pg_repack">Database Efficiency 101</a> goes deeper on the bloat side of this; the piece worth adding here is that vacuum isn't an isolated background job, it's sharing the same WAL and I/O path as everything above.</p>
<h2>ANALYZE: the step that keeps the planner honest</h2>
<p><code>ANALYZE</code> is a separate operation from vacuum, even though <code>autovacuum</code> triggers both and people often say "autovacuum" to mean either. <code>ANALYZE</code> samples a table's rows and updates the planner's statistics (row counts, most common values, distribution histograms) stored in <code>pg_statistic</code>. The query planner uses those statistics, not a live count, to estimate how many rows a <code>WHERE</code> clause will match and decide whether a sequential scan, index scan, or bitmap heap scan will be cheapest.</p>
<p>Stale statistics don't cause a query to return wrong results. They cause the planner to misjudge how many rows it's dealing with, and a bad row-count estimate is one of the most common root causes behind "this query used to be fast and now it isn't," with no schema change and no data corruption involved, just a planner making a good decision on outdated information. <code>autovacuum_analyze_threshold</code> and <code>autovacuum_analyze_scale_factor</code> (10% by default) control when this runs automatically, similarly to vacuum's thresholds, and an insert-heavy, delete-free table (an append-only events table, for instance) still needs <code>ANALYZE</code> on a schedule even though it may rarely need vacuuming for dead tuples.</p>
<p><strong>Why this matters in production:</strong> after a large bulk load, either via a migration or an initial data import, running <code>ANALYZE</code> explicitly before serving real traffic is worth the few seconds it costs. Waiting for autovacuum to notice on its own schedule means the planner may make its first few hours of decisions on statistics from before the table had any real data in it.</p>
<h2>Putting the five back together</h2>
<p>None of these are separate systems bolted onto Postgres. They're one pipeline: WAL makes every write durable and replicable, checkpoints periodically settle that WAL's guarantees into the actual data files and bound how long crash recovery takes, wait events are the live diagnostic surface showing you when a backend is stuck somewhere in that path, vacuum reclaims the space MVCC leaves behind and prevents wraparound, and <code>ANALYZE</code> keeps the planner's model of your data close enough to reality to keep picking good plans. A slow query is sometimes a bad query. Just as often, in a system that's been running fine for months, it's one of these five processes falling behind, and <code>pg_stat_activity</code> is the first place that tells you which one.</p>]]></content:encoded>
      <pubDate>Mon, 14 Sep 2026 05:05:17 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>WAL</category>
      <category>Vacuum</category>
      <category>Performance</category>
      <category>Backend</category>
      <category>SQL</category>
    </item>
    <item>
      <title>Your ORM Isn&apos;t Lying to You. It&apos;s Just Not Telling You Everything.</title>
      <link>https://insight.jatinjainsaraf.com/postgresql-orms-what-they-hide</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/postgresql-orms-what-they-hide</guid>
      <description>ORMs like Prisma and TypeORM don&apos;t lie to you about your database, they just don&apos;t tell you everything. Here&apos;s where that gap shows up in production: N+1 queries, migrations that silently drop columns, connection pool exhaustion in serverless, and the SQL your ORM actually generates versus what you think it&apos;s running.</description>
      <content:encoded><![CDATA[<p>An ORM is a translator sitting in a business meeting. It's fluent, it's fast, and after a while you stop paying attention to the original language entirely. You trust it. Most days that trust is fine. Then one sentence gets mistranslated, the deal falls apart, and you realize you never actually learned the language yourself. You just learned to rely on someone who did.</p>
<p>That's what Prisma, Drizzle, TypeORM, and Sequelize do to your relationship with Postgres. They don't lie. They just don't tell you everything, and the gaps are exactly where production incidents live.</p>
<p>This post is not "ORMs are bad, use raw SQL." Most apps should use one. It's a map of the specific places where the ORM's model of your database and Postgres's actual behavior diverge, so you know where to look when something is slow, wrong, or gone.</p>
<h2>Why teams reach for an ORM in the first place</h2>
<p>Before the shortcomings, the honest case for using one:</p>
<ul>
<li><strong>Types generated from your schema.</strong> Rename a column, your build breaks at compile time instead of at 2am in production.</li>
<li><strong>Migrations as code.</strong> Schema changes are version-controlled, reviewable in a pull request, and reproducible across environments.</li>
<li><strong>Boilerplate CRUD disappears.</strong> Most application code touching the database is <code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code> on one or two tables. An ORM turns that into a few lines instead of a hand-written query and a row-mapping function, every time.</li>
</ul>
<p>None of that is wrong. The problem is what it costs you when you stop thinking about the SQL underneath.</p>
<h2>The N+1 query, hiding in plain sight</h2>
<p>This is the failure mode that catches the most teams, because the ORM makes it look identical to the correct version.</p>
<pre><code class="language-javascript">const users = await prisma.user.findMany();
for (const user of users) {
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}
</code></pre>
<p>That's 1 query to fetch users, plus N more queries, one per user, to fetch their posts. With 10 users you might not notice. With 10,000 users, those extra round trips can turn an otherwise simple endpoint into a timeout.</p>
<p>The dangerous part isn't that this pattern exists. Every engineer knows N+1 queries are bad. The dangerous part is that the ORM's relational syntax looks the same whether it's doing this or a single join:</p>
<pre><code class="language-javascript">const users = await prisma.user.findMany({
  include: { posts: true },
});
</code></pre>
<p>Relation loading isn't necessarily equivalent to the join you would write by hand. Depending on the ORM and its configuration, that one line can compile down to a single query with a join, or to a batch of separate queries stitched together in application code. Prisma, for example, defaults to the second approach for <code>include</code> on one-to-many relations: it batches the related rows with a SQL <code>IN</code> clause rather than emitting a native <code>JOIN</code>, unless you're on a relation mode or raw query that says otherwise. You can't reliably tell which one you got just by reading the application code. Turn on query logging and look.</p>
<p><strong>Why this matters in production:</strong> N+1 doesn't necessarily mean N new connections. Queries can reuse the pool. The problem is the number of database round trips. Under concurrent traffic, many requests running their own N+1 loops can keep pooled connections busy for longer, increasing pool contention and request latency. This is where an ORM's convenience directly creates the incident.</p>
<h2>The SQL your ORM actually generates is not the SQL you'd write</h2>
<p>Turn on query logging for a week on a real Prisma or TypeORM project and read what comes out. It is rarely what you'd write by hand.</p>
<p>With nested <code>include</code>s, some ORMs, Prisma among them, can issue several separate queries and stitch the results together in application code instead of one query with joins. That's not a bug, it's a design trade-off, and it varies by ORM, version, and configuration. But it means <code>EXPLAIN ANALYZE</code> on the query you think you're running and the query Postgres actually saw can tell two different stories, and only one of them is visible in your codebase.</p>
<p><strong>Why this matters in production:</strong> if you've read through the <a href="/courses/postgresql-in-depth">query planning module</a> of the PostgreSQL In-Depth course, you know Postgres picks between a sequential scan, an index scan, and a bitmap heap scan based on cost estimates for the <em>specific query it receives</em>. An ORM that reshapes your intent into three smaller queries instead of one joined query gives the planner three separate decisions to make, rather than one plan for the operation as a whole. You lose the planner's ability to optimize across the whole operation.</p>
<p>The fix isn't abandoning the ORM. It's treating query logging as mandatory, not optional, and periodically running <code>EXPLAIN ANALYZE</code> on queries that matter to your application's hot paths or have noticeable latency.</p>
<h2>Migrations: the auto-diff can silently drop data</h2>
<p>Most ORMs generate migrations by diffing your schema file against the last known state and producing SQL. This works well for additive changes. It gets dangerous on renames.</p>
<p>Rename a column in your schema file, and a schema diff doesn't always have enough information to know that you intended a rename rather than a drop-and-add. Some ORMs can detect renames, while others may generate a drop-and-add migration depending on the schema change and migration workflow. The generated migration can be:</p>
<pre><code class="language-sql">ALTER TABLE users DROP COLUMN full_name;
ALTER TABLE users ADD COLUMN display_name text;
</code></pre>
<p>instead of the safe version:</p>
<pre><code class="language-sql">ALTER TABLE users RENAME COLUMN full_name TO display_name;
</code></pre>
<p>The first version runs without error. It also silently discards every value in that column on deploy.</p>
<p><strong>Why this matters in production:</strong> a migration that runs clean and drops a column's worth of data doesn't announce itself. Nobody gets an error. You find out when a support ticket comes in asking why a field is suddenly empty. This is one of the highest-leverage things to check before merging any ORM-generated migration: read the actual SQL it produces, not just the schema diff you wrote.</p>
<h2>Connection pooling: every ORM has different defaults, and it matters more than ever in serverless</h2>
<p>If you've read <a href="/blog/connection-pooling-in-the-serverless-era-five-failure-modes">Connection Pooling in the Serverless Era</a>, you already know that each connection Postgres accepts consumes server resources, whether or not it's doing anything, and enough concurrent connections becomes a significant resource cost. ORMs each ship their own default pool size, their own idle-timeout behavior, and their own assumptions about how long your process lives.</p>
<p>Those defaults were mostly written assuming a long-lived server process. In a serverless or edge function, where every invocation can be a fresh process, an ORM's default pool of, say, 10 connections means every cold start tries to open 10 connections to Postgres. Multiply that by concurrent invocations during a traffic spike and you can exhaust your database's max connection limit in seconds, well before you exhaust CPU or memory on the compute side.</p>
<p><strong>Why this matters in production:</strong> this is a common cause of "it works locally, it falls over under load" for teams deploying ORM-based apps to serverless platforms. The fix usually involves an external pooler (PgBouncer, RDS Proxy, or a managed equivalent) sitting between your functions and Postgres, plus explicitly tuning the ORM's own pool size down, not trusting the default. One trap worth knowing before you reach for a pooler: PgBouncer's transaction-mode pooling, the mode most serverless setups need, doesn't preserve a session across queries, and ORMs that rely on prepared statements (Prisma and TypeORM both do, by default) can fail against it in ways that only show up under load. Check your pooler's mode against your ORM's prepared-statement behavior before you assume adding one fixes everything.</p>
<h2>Write-heavy systems: where the ORM's convenience becomes the bottleneck</h2>
<p>Everything above assumes a fairly typical application: reads dominate, writes happen when a user submits a form or an admin edits a record. That's most CRUD apps, and it's exactly where an ORM's ergonomics pay for themselves. It is not a blockchain indexer, an event pipeline, or anything else ingesting a continuous, high-volume stream of writes. Those systems are where an ORM's row-at-a-time model of the world stops being a convenience and starts being the bottleneck.</p>
<p>A blockchain indexer is a clean example because the write pattern is unforgiving: every new block can carry hundreds or thousands of events (transfers, contract calls, state changes), and all of them need to land in Postgres before the indexer can call itself caught up. Three places an ORM adds real cost in that path:</p>
<ul>
<li><strong>Row-by-row <code>INSERT</code> instead of <code>COPY</code>.</strong> Postgres's <code>COPY</code> protocol is built specifically for loading large volumes of rows in one streamed operation, and it is meaningfully faster than even a well-batched multi-row <code>INSERT</code>. Most ORMs don't expose <code>COPY</code> at all. Their bulk-insert APIs (<code>createMany</code>, <code>bulkCreate</code>, and similar) are a real improvement over inserting one row per statement, but under the hood they're still building <code>INSERT</code> statements with many value tuples, not streaming through <code>COPY</code>. For an indexer processing thousands of events per block, that gap compounds every single block.</li>
<li><strong>Object hydration on the way in and out.</strong> An ORM that maps every row to and from a model instance is doing real work per row: validation, type coercion, applying defaults. That cost is invisible at CRUD volumes and very visible at ingestion volumes, where it adds CPU and allocation overhead to every event in the stream, not just the ones a human is waiting on.</li>
<li><strong>More, smaller transactions than the workload needs.</strong> An ORM's ergonomic defaults tend toward one transaction per logical operation. At indexer-scale throughput, that can mean far more transaction and WAL overhead than a design that deliberately batches writes into fewer, larger transactions per block.</li>
</ul>
<p><strong>Why this matters in production:</strong> all three of these show up as the same symptom eventually, insert throughput that can't keep up with the source it's indexing, and the indexer falls further behind with every block. If you've read <a href="/blog/taming-postgresql-replication-lag-in-real-time-blockchain-indexers">Taming PostgreSQL Replication Lag in Real-Time Blockchain Indexers</a>, this is the same category of problem from a different angle: it's not always replication configuration that causes an indexer to lag, sometimes it's the write path itself, one ORM abstraction away from the bulk-loading tools Postgres actually gives you for this job.</p>
<p>None of this means an ORM is disqualified from a write-heavy system. It means the write path deserves the same scrutiny as any other hot path in this article: measure it, and if it's the bottleneck, that's exactly the place to drop to <code>COPY</code>, hand-batched multi-row <code>INSERT</code>s, or a dedicated bulk-loading library, and keep the ORM everywhere else it's not in the way.</p>
<h2>When the abstraction stops being useful</h2>
<p>Every ORM's query builder is designed around the common case: filter, sort, join, paginate. The moment your requirement moves past that, you're negotiating with the abstraction instead of using it. In practice, that means:</p>
<ul>
<li><strong>Window functions</strong> (running totals, ranking within groups) — most ORM query builders don't model these at all.</li>
<li><strong>Recursive CTEs</strong> (org charts, threaded comments, category trees) — almost always raw SQL, even in projects that otherwise avoid it entirely.</li>
<li><strong>Bulk operations</strong> — an ORM's <code>.update()</code> in a loop is N statements; a single <code>UPDATE ... WHERE id = ANY($1)</code> is one. The difference compounds fast on large batches.</li>
<li><strong><code>ON CONFLICT</code> (upsert) and <code>RETURNING</code></strong> — supported by some ORMs, but often with a narrower API than the SQL clause itself allows, especially bulk upserts (<code>ON CONFLICT DO UPDATE</code> across many rows at once) and the newer <code>OLD</code>/<code>NEW</code> aliases in <code>RETURNING</code>.</li>
<li><strong><code>SKIP LOCKED</code> for queue-style workloads</strong> — the standard pattern for letting multiple workers pull from the same table without blocking on each other's locked rows. Most ORM query builders have no vocabulary for it at all, so it's raw SQL from the start.</li>
<li><strong>Partial and expression indexes</strong> — PostgreSQL can use indexes defined with a <code>WHERE</code> clause or an expression, but the generated SQL still needs to satisfy the conditions that make those indexes usable. An ORM won't necessarily make that obvious from the application code.</li>
<li><strong>JSONB queries with GIN indexes</strong> — the operators that make these fast (<code>@></code>, <code>?</code>, <code>#>></code>) are Postgres-specific and rarely have first-class ORM support.</li>
<li><strong>Row-level and advisory locking</strong> — <code>SELECT ... FOR UPDATE</code>, <code>pg_advisory_lock</code>, and friends are concurrency primitives most ORMs expose thinly or not at all.</li>
<li><strong>Reading <code>EXPLAIN ANALYZE</code> output</strong> — no ORM will do this for you. It's the one skill that stays entirely yours no matter which tool you pick.</li>
</ul>
<p>None of this means avoid the ORM for these cases. It means know, ahead of time, that you'll drop to raw SQL here, and treat that as normal rather than a sign something went wrong.</p>
<h2>Is your ORM a black box? It depends which one</h2>
<p>So what should you actually use?</p>
<p>Not all ORMs hide the same amount, and the trade-off usually runs opposite to how much boilerplate they remove:</p>









































<table><thead><tr><th>Approach</th><th>Abstraction</th><th>SQL visibility</th><th>Best fit</th></tr></thead><tbody><tr><td>Prisma</td><td>High</td><td>Medium</td><td>CRUD-heavy product development</td></tr><tr><td>TypeORM</td><td>High</td><td>Medium</td><td>Traditional TypeScript applications, decorator-based models</td></tr><tr><td>Sequelize</td><td>High</td><td>Medium</td><td>Mature Node.js codebases already built around it</td></tr><tr><td>Drizzle</td><td>Medium</td><td>High</td><td>Type-safe apps that still want SQL-shaped queries</td></tr><tr><td><code>pg</code> / <code>node-postgres</code></td><td>Low</td><td>Full</td><td>SQL-heavy code and hot paths</td></tr></tbody></table>
<p>There's no winner in that table on purpose. A team shipping CRUD-heavy internal tools benefits enormously from Prisma's or Sequelize's ergonomics and rarely hits the edges above. A team running a write-heavy, high-traffic public API needs to know exactly which SQL is running on every hot path, and might be better served by Drizzle or hand-written queries where it matters most. Plenty of production codebases mix approaches deliberately: an ORM for the CRUD majority, raw SQL for the handful of queries that are actually hot.</p>
<h2>The actual takeaway</h2>
<p>The ORM isn't the problem. Forgetting that an ORM is generating database operations is the problem. It doesn't remove the need to understand what Postgres is doing, it just moves the moment you're forced to learn it, from day one of the project to the incident where your database is drowning in unnecessary queries because the ORM issued forty small <code>UPDATE</code> statements instead of one bulk operation.</p>
<p>Use the ORM for what it's good at: the 80% of your code that's routine CRUD, type-safe and fast to write. But keep query logging on, read the SQL your ORM generates for anything on a hot path, and read the actual migration SQL before it hits production, not just the schema diff. The ORM is a very good translator. It is still worth knowing enough of the language yourself to catch it when it gets a sentence wrong.</p>]]></content:encoded>
      <pubDate>Thu, 03 Sep 2026 13:32:46 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>ORM</category>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Backend</category>
      <category>Performance</category>
      <category>SQL</category>
      <category>Prisma</category>
    </item>
    <item>
      <title>A Reliability-Hardening Sprint: Isolation, Guardrails, and a Query Plan That Went From 45 Million to 206</title>
      <link>https://insight.jatinjainsaraf.com/case-study/a-reliability-hardening-sprint</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/a-reliability-hardening-sprint</guid>
      <description>None of these fixes were triggered by an outage. Per-network cache isolation, timeouts on every resolver, dead-code removal, a partition-pruning fix with a genuinely startling before/after number, and a real external security report handled the right way.</description>
      <content:encoded><![CDATA[<h1>A Reliability-Hardening Sprint: Isolation, Guardrails, and a Query Plan That Went From 45 Million to 206</h1>
<p>The easiest reliability work to justify is the kind that follows an outage. It's much harder to carve out time for the kind that doesn't, fixes made because a risk is visible and worth closing, not because it already caused damage. Most of what's in this piece shipped in a single week without a single one of these changes being a response to a live incident. One of them was a response to an external report, handled the way you'd want it handled. Together they're a good picture of what proactive reliability work actually looks like, as opposed to the reactive kind that gets written up more often because it comes with a dramatic story attached.</p>
<h2>Deciding which environments actually need their own failure domain</h2>
<p>Multiple blockchain environments were originally sharing a single default Redis instance for caching. That's a reasonable starting point when the system is small, and a real liability once one environment's traffic pattern starts affecting another's: a cache invalidation problem, a memory pressure spike, or a slow operation in one environment can degrade or contaminate the cache for every other environment sharing that same instance, even though those environments have nothing to do with each other functionally. The fix wasn't to give every environment its own instance unconditionally, it was to identify which ones actually warranted the isolation and pull just those out: mainnet and one specific microchain, the one carrying enough independent traffic and importance to justify it, each got their own dedicated Redis instance, enforced strictly enough in code that the service refuses to start if that instance's address isn't configured. Everything else still shares a common default instance. Nothing about this fix was triggered by an incident. It closed a risk that was visible in the architecture before it had the chance to become one, and it did so by isolating exactly the environments that needed it rather than paying the operational cost of isolating all of them.</p>
<h2>A timeout is a blast-radius decision, not a performance tweak</h2>
<p>Every GraphQL resolver in the backend service, every query and every mutation, got wrapped with a wall-clock timeout, applied schema-wide rather than added resolver by resolver. It's tempting to think of a timeout as a performance optimization, something you add to make slow things feel faster. That's not really what this fix does. Its actual job is bounding how much damage a single slow or hanging query can do to everything around it: without a timeout, one resolver stuck waiting on a slow database call or a hung upstream request can hold a connection, a worker thread, or a piece of shared capacity indefinitely, degrading the whole service for requests that have nothing to do with the slow one. A timeout doesn't make the underlying slow query fast. It makes sure that whatever caused it to be slow stays contained to that one request instead of spreading.</p>
<h2>The number worth quoting on its own</h2>
<p>A separate fix, found during a broader review of query performance, is the single most quotable result in this entire piece. A function fetching network-wide transaction statistics was filtering its query by a timestamp column instead of the table's actual partition key. That distinction sounds academic until you see what it costs: filtering on the wrong column meant Postgres couldn't use partition pruning at all, forcing a full scan across every partition, including one that alone held over 400 gigabytes of data. The query planner's own estimated cost for that query, captured at the time, was just under 45 million. Correcting the filter to use the actual partition key was the first fix, and it dropped that same estimated cost to 206, from a query the planner expected to be genuinely expensive to one it treated as nearly free from a single change of which column a filter clause referenced. The system has since moved a step further than that filter fix. The current version of this function doesn't scan the partitioned transaction table at all, even with a correct filter: it reads instead from a separate, precomputed stats snapshot table, sidestepping the scan entirely. The comment left in that code still cites the original ~45 million cost as the reason the snapshot approach exists, a permanent, visible record of the problem the current design was built to avoid.</p>
<p>A related fix in the same family addressed a query fetching transaction lists that was missing partition-key predicates specifically inside its join clauses, which defeated the database's runtime partition pruning and forced scans across well over a hundred partitions where only a small number should have been touched. Both fixes are variations on the same underlying lesson: a partitioned table only delivers its performance benefit if every query touching it, including the parts buried inside joins and CTEs, actually filters on the partition key. Miss it in one place, and the partitioning strategy silently stops helping for that specific query while still looking correct.</p>
<h2>An external report handled the right way</h2>
<p>Not every fix here was found internally. An unauthenticated search endpoint was found, by an external report rather than an internal audit, to accept oversized input in a way that could trigger server errors. The response was layered rather than a single patch: a hard length cap on the search input rejects oversized requests outright before they reach anything expensive, the cache key for a search result is now a hash of the input rather than the input itself, so even a maliciously oversized string can't be used to construct an arbitrarily large cache key and exhaust cache memory that way, and a dedicated GraphQL security library was added on top, enforcing limits on query depth, aliases, and token count, with batched HTTP requests disabled outright so those same limits can't be sidestepped by sending an array of operations instead of one. The one control deliberately left off was a cost-limiting layer, everything else in that same security library was turned on. The detail worth noting isn't the specific fix, it's that an externally-reported issue got a defense-in-depth response instead of the narrowest possible patch that would have made the specific report stop reproducing.</p>
<h2>The small bug that's a reminder about write-path consistency</h2>
<p>One more fix, smaller in scope but worth including for what it represents: an address being inserted into a detail table without the padding format that the corresponding foreign-key target table always expected, causing insert failures against that constraint. It's a small, almost mundane bug, but it's a recognizable category: two different code paths writing related data that quietly disagree about a formatting convention neither one documented explicitly. It's the same underlying failure shape as the type-mismatch issues described elsewhere in this series, just at a smaller scale and caught before it caused a bigger one.</p>
<h2>What this demonstrates</h2>
<p>None of these seven changes, cache isolation, resolver timeouts, two separate partition-pruning fixes, a security hardening response, and a small write-path consistency fix, are individually dramatic. What makes them worth writing up together is that none of them were forced by an outage severe enough to demand attention. They were found by looking, in the middle of a normal week, for risks that hadn't yet become incidents, and closing them anyway. That's a much less visible kind of engineering work than fixing something already on fire, and it's usually the reason there's less to write up after the fact, not because less happened, but because nothing broke.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:58:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>reliability</category>
      <category>graphql</category>
      <category>redis</category>
      <category>postgresql</category>
    </item>
    <item>
      <title>A Type Migration Across Three Services: VARCHAR to BIGINT at Scale</title>
      <link>https://insight.jatinjainsaraf.com/case-study/a-type-migration-across-three-services-varchar-to-bigint-at-scale</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/a-type-migration-across-three-services-varchar-to-bigint-at-scale</guid>
      <description>The migration itself was the easy part. The real risk was every downstream function quietly assuming a column would always come back as a string. Two hotfixes later, that assumption was gone.</description>
      <content:encoded><![CDATA[<h1>A Type Migration Across Three Services: VARCHAR to BIGINT at Scale</h1>
<p>A block height stored as text instead of a number sounds like a small, easily-forgiven decision, and for a long time it is. The chain's own RPC layer returns numeric values like block height as strings in the first place, because some of those numbers exceed what can be safely represented as a standard number in JavaScript, so storing them as text on the way in feels like the path of least resistance. The cost of that decision doesn't show up until someone runs a range query and Postgres compares those "numbers" alphabetically instead of numerically, silently returning wrong results. Fixing that meant migrating the column's actual type from text to a proper integer at its source, the indexer that originally writes it, and then getting every downstream reader of that same column to agree with the new type. The migration itself turned out to be the least risky part of the whole change.</p>
<h2>Why the column type was the easy part</h2>
<p>Changing a column's underlying type in Postgres is a well-understood operation. The genuine risk in a change like this isn't the <code>ALTER TABLE</code> itself, it's every piece of application code downstream that was written against the old type's behavior without ever stating that assumption explicitly. A function that concatenates a block height into a string for logging, a comparison that relies on lexicographic ordering without realizing it, a serialization layer that formats a string differently than it formats a number, none of these show up as compile errors. They show up as runtime bugs, in production, only when that specific code path executes against the newly-typed column for the first time.</p>
<p>That's exactly what happened here. The migration itself landed cleanly across the backend service and the socket server, both of which read from the same underlying column. Two follow-up hotfixes were needed shortly after, both in functions that hadn't been touched by the migration directly but that broke because they'd been implicitly relying on the column's old string type when computing values like the latest block height and network-wide statistics. Neither hotfix was a large fix. Both were necessary, and both were the direct, predictable cost of a type change rippling through code that had never explicitly declared its assumption about that type in the first place.</p>
<h2>What coordinating across services actually requires</h2>
<p>The reason this qualifies as a coordination problem, not just a database migration, is that three services, the indexer writing the column, and the backend and the socket server reading it, are all deployed independently, on their own release cycles, each with its own copy of code that depends on this column's type. A schema change made once, in one place, has to be correctly consumed by every service reading that data, and there's no compiler shared across service boundaries to catch a mismatch between what the database now returns and what a given service's code still expects. The two hotfixes here are exactly what "coordination worked, but wasn't perfect on the first attempt" looks like in practice: the underlying migration itself never had to be rolled back or redone, but the code on the consuming side needed a second pass once the new type actually reached it in production.</p>
<h2>What this demonstrates</h2>
<p>The lesson here isn't "test your migrations more," though that's true of any migration. It's that a type change at the schema level is never really contained to the schema. It's a contract change with every piece of code, across every service, that reads that column, and the services most likely to break are the ones that were never explicitly written against a documented type contract in the first place, because they were written back when the column's actual type was assumed rather than verified. Catching both breakages quickly, shipping targeted hotfixes rather than a broader rollback, and confirming the underlying migration itself held throughout is the realistic version of what "coordinating a schema change across services" looks like when it goes well: not zero surprises, but small, contained ones, caught and fixed fast.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:57:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>postgresql</category>
      <category>schema-migration</category>
      <category>distributed-systems</category>
    </item>
    <item>
      <title>Built for a Multi-VM Future: The RPC v4 Readiness Work</title>
      <link>https://insight.jatinjainsaraf.com/case-study/built-for-a-multi-vm-future-the-rpc-v4-readiness-work</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/built-for-a-multi-vm-future-the-rpc-v4-readiness-work</guid>
      <description>A block that mixes Move and EVM transactions is coming. Scoping exactly what breaks across five layers of the stack, before the feature is fully live, is the actual work of readiness engineering.</description>
      <content:encoded><![CDATA[<h1>Built for a Multi-VM Future: The RPC v4 Readiness Work</h1>
<p>Some engineering work happens in response to an incident. This work happened in response to a feature that hadn't fully shipped yet. Supra's chain infrastructure introduced a new RPC version explicitly designed to support multiple virtual machines processing transactions within the same block, Move and EVM to start, with more planned. At the time this readiness work began, the indexing stack was still built entirely around the older RPC version and its Move-only assumptions. The task wasn't fixing something broken. It was mapping, precisely, everything that would break once the new capability went fully live, before it did.</p>
<h2>A wrapper key that touches everything downstream</h2>
<p>The documented change between RPC versions looks small in isolation: several top-level fields on a transaction, the sender, the payload, the output, the authenticator, gain a wrapper key identifying which virtual machine produced that data. The interesting part is that the codebase's own type definitions have already started anticipating this shape ahead of the actual rollout: the response types for these fields already model a VM-keyed wrapper alongside the currently-used one, with a placeholder branch for a second VM sitting unused in the type layer today, well before that second VM's data ever arrives over the wire. That's a reasonable way to de-risk a breaking change, let the types absorb the new shape early, in a place where being wrong costs a compile error, rather than doing it all at once under time pressure once the new format is actually live.</p>
<p>Type-level readiness isn't the same as working end-to-end, though, and that gap is the real scope of what's left. Every place in both the indexer and the backend that reads a transaction's sender, payload, output, or authenticator still needs to actually branch on which VM produced it and unwrap accordingly, across multiple files in both repositories, not just have a type that permits doing so. Event parsing, which currently reads from one fixed location in the response, needs equivalent VM-aware branching. None of these are individually difficult changes. All of them need to happen together, correctly, and be exercised against real second-VM data before the system can claim genuine multi-VM support rather than a type layer that's merely ready for it.</p>
<h2>Why this is a cross-cutting change, not a parser update</h2>
<p>The reason this readiness work spans far more than the RPC-consuming code is what multi-VM support actually means at the data layer. A single block will be able to contain transactions from more than one virtual machine simultaneously, and those transaction types don't share a schema. An EVM transaction's meaningful fields, sender, recipient, calldata, value, look nothing like a Move transaction's sender, function, and payload. That difference has to be handled at every layer: the indexer needs a distinct parser per virtual machine, plus a column identifying which VM produced each row, since a single flat schema can't represent both shapes cleanly. The database question is still genuinely open rather than settled: either add a dedicated table per virtual machine, one for EVM alongside the existing Move-shaped ones, and eventually a Solana or Sui equivalent, or keep one shared transaction table and add a JSONB column to hold whatever VM-specific fields don't fit the common shape. Neither is free. Separate tables keep every VM's data cleanly typed and indexed but mean every cross-VM query, "show me the last hundred transactions regardless of which VM ran them," has to union across tables that don't share a schema. A shared table with a JSONB escape hatch keeps queries simple at the cost of reintroducing exactly the kind of untyped, hard-to-index column this same series describes paying down elsewhere. That tension hasn't been resolved yet, and naming it as unresolved is more honest than picking a side before the second VM actually arrives and reveals which cost matters more in practice. Even something as basic as the wallet triggers described elsewhere in this series need updating regardless of which path gets picked, because EVM addresses use a different length and format than Move addresses, and any logic keyed on address shape needs to handle both correctly. The backend needs VM-aware logic in every resolver that returns transaction details, and the frontend needs to render two structurally different transaction types side by side in the same block view, with address formatting that doesn't assume every wallet address looks the same.</p>
<h2>Doing the scoping before the deadline, not during it</h2>
<p>At the time of this work, a multi-VM development network already existed and was publicly reachable, with a frontend deployed against it, but the actual multi-VM parsing, schema, and UI work hadn't been built yet, meaning that live deployment likely only reflected Move transactions in practice despite technically running against multi-VM-capable infrastructure. The rollout plan follows a pattern the team had already used successfully once before, when the previous RPC version transition happened: enable the new endpoint behind a feature flag on the testnet environment first, validate the full indexing pipeline end to end against real data in that shape, and only then flip the same flag on production once the chain itself makes the new version available there.</p>
<h2>What this demonstrates</h2>
<p>The value in this piece of work isn't a shipped feature, because at the time of writing, the full multi-VM parsing and schema support hadn't been built yet either. The value is in having already mapped, field by field and file by file, exactly what breaks and where, ahead of the deadline that would otherwise force that mapping to happen under pressure. Readiness work like this rarely gets noticed when it goes well, because going well looks like nothing happened: the feature arrives, the flag flips, and the system handles it. The alternative, discovering the breaking changes live, in production, once transactions from a second virtual machine actually start arriving, is the outcome this scoping work exists specifically to prevent.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:56:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>blockchain</category>
      <category>multi-vm</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Becoming the Readiness Gate for Another Team&apos;s Scale-Up</title>
      <link>https://insight.jatinjainsaraf.com/case-study/becoming-the-readiness-gate-for-another-teams-scale-up</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/becoming-the-readiness-gate-for-another-teams-scale-up</guid>
      <description>One team validated their system could handle 100,000 concurrent tasks. They still wouldn&apos;t turn the dial on a live network until a different team&apos;s indexer proved it was ready. That&apos;s a real dependency, not a hypothetical one.</description>
      <content:encoded><![CDATA[<h1>Becoming the Readiness Gate for Another Team's Scale-Up</h1>
<p>Most engineering work gets justified by its own metrics: faster, cheaper, more reliable. Some of the most consequential work gets justified by someone else's roadmap depending on it. That was the actual shape of the concurrency and trigger-removal work described elsewhere in this series. It wasn't scoped because SupraScan's own dashboards demanded it. It was scoped because a separate team, building automated on-chain task execution, had explicitly tied their own launch decision to it.</p>
<h2>A capability that was ready, gated by a system that wasn't yet</h2>
<p>The automation team had already done real validation work of their own: their Automation V2 system had been privately tested to handle 100,000 concurrent tasks on a private test network. That's not a small number, and it's not a hypothetical claim either, it was measured. And yet, that team explicitly agreed not to raise the live, on-chain capacity limits for real automated tasks on a real network until the indexer's ability to handle that scale was confirmed.</p>
<p>That's an unusual kind of commitment for one team to make around another team's infrastructure, and it only makes sense once you understand what those capacity limits actually control. The chain enforces a default cap on how many tasks a single account can register, alongside a separate cap on the total gas budget those tasks can consume, plus a lower, distinct ceiling for tasks registered through a governance process rather than a normal account. None of these caps are hard-coded ceilings in the underlying protocol logic; every one of them is adjustable through a standard configuration update, with no code change required. The only thing standing between the current limit and a much higher one is a decision to raise it, which means the real gate on scaling automation usage was never a protocol constraint. It was confidence that the systems consuming the resulting transaction volume, this indexer chief among them, wouldn't buckle the way it already had once, during the four-wallet contention incident that first exposed the trigger-locking problem this same work fixed.</p>
<h2>Why the dependency was structurally sound, not just cautious</h2>
<p>It would be easy to read this as one team simply being conservative. The more precise read is that the dependency was well-scoped because of how automation execution actually behaves on-chain: each registered task executes at most once per block. That means the worst-case number of executions in any single block scales linearly with whatever the capacity cap is set to, not multiplicatively or unpredictably. A team raising that cap can reason precisely about the additional load it introduces, which is exactly the kind of predictable, boundable increase that makes "we'll raise this once the downstream system is proven ready" a decision you can actually commit to, rather than a vague promise to be careful later.</p>
<p>That precision is also what made this indexer's readiness work legible as a real gate rather than a soft suggestion. The automation team didn't need to trust a general assurance that "the indexer should be fine." They could point to a specific configuration value, know exactly how it maps to worst-case block-level load, and wait for confirmation that the exact failure mode which had already happened once, contention on a small number of hot wallet rows under concurrent load, had been genuinely closed.</p>
<h2>What this demonstrates</h2>
<p>The trigger-removal and concurrency work described in this series would have been worth doing on its own merits, the four-wallet incident alone justified it. What makes it a more interesting case study than a standard reliability fix is that a different team's product roadmap was explicitly, formally waiting on it. That's a different kind of leverage than fixing your own system's stability. It's being the reason someone else's already-validated capability sat unused on a real network until the work described here shipped, verified, across every environment. Infrastructure work rarely gets to point at a concrete, named dependency like that. When it does, it's worth naming directly instead of folding it quietly into a general reliability narrative.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:55:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>cross-team</category>
      <category>blockchain</category>
      <category>automation</category>
    </item>
    <item>
      <title>What It Would Take to Hit 500K TX/Sec, and Why That&apos;s the Honest Answer</title>
      <link>https://insight.jatinjainsaraf.com/case-study/what-it-would-take-to-hit-500k-tx-sec</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/what-it-would-take-to-hit-500k-tx-sec</guid>
      <description>Every scaling lever on the table, measured honestly, still lands 25x short of a 500,000 tx/sec target. Here&apos;s the capacity-planning math, and why the right answer was to say so instead of overselling a smaller win.</description>
      <content:encoded><![CDATA[<h1>What It Would Take to Hit 500K TX/Sec, and Why That's the Honest Answer</h1>
<p>Capacity planning conversations have a predictable failure mode: someone names a big target number, and the answer that gets built is optimized for sounding capable of hitting it, not for being honest about whether it actually can. This is the opposite of that. A benchmark measured SupraScan's sustained throughput at the time, three concrete scaling levers were evaluated with honest multipliers instead of aspirational ones, and the combined answer, done as carefully as possible, still fell 25 times short of a 500,000 transaction per second target. The useful output of this exercise wasn't a bigger number. It was knowing exactly why that number wasn't reachable with this architecture, and what would actually be required if it mattered enough to pursue.</p>
<h2>The baseline, and where the bottleneck actually was</h2>
<p>At the time of this planning exercise, a benchmark run measured roughly 110 to 118 transactions per second sustained, across ten running instances. The instinct when a number looks low is to assume the database is the constraint. It wasn't. In the same benchmark window, Postgres itself sat at 50 percent CPU idle, meaning it had considerable spare capacity at that load. The actual bottleneck was the RPC layer: 34 socket-hangup errors inside a 37-second window, with latency climbing past 600 milliseconds. The system wasn't write-constrained. It was constrained by how fast data could be safely pulled out of the chain node in the first place.</p>
<h2>Three levers, evaluated without rounding up</h2>
<p>Three concrete changes were assessed, each given a multiplier grounded in what it actually does rather than what would sound good in a roadmap slide.</p>
<p>Switching from a pull-based RPC pattern, roughly two RPC calls per block today, to a push-based WebSocket feed removes the single biggest blocker at the current scale: a thundering-herd effect on the RPC endpoint that gets worse as more instances are added, and which currently caps how many instances can even run productively. But this lever is explicitly a ceiling-remover, not a throughput multiplier on its own. It adds zero additional database write capacity by itself; it only clears the obstacle currently in front of scaling out further.</p>
<p>Sharding write traffic is the only one of the three levers that actually multiplies database capacity, because the current architecture writes through a single Postgres primary. Splitting into 8 to 16 shards was judged a plausible, though explicitly not linear, 8 to 16x increase in write ceiling. The honest caveats matter here: cross-shard queries, anything resembling a leaderboard spanning many wallets, get meaningfully harder once data is split this way, and a real, observed pattern of hot-shard skew, one specific address firing continuous automated self-transactions, actively eats into the theoretical gain by concentrating load unevenly across whichever shard that address lands on.</p>
<p>Replacing trigger or sync-based wallet counting with write-ahead-log-derived counters targets the hot-row contention problem directly, the same contention responsible for the multi-day lag incident described elsewhere in this series. Its honest impact is bounded: an estimated 20 to 50 percent reduction in per-transaction database work, explicitly not a throughput multiplier in its own right.</p>
<h2>The math, done honestly, and the verdict it produces</h2>
<p>Combining all three levers, removing the RPC bottleneck, applying an 8 to 16x sharding gain, and applying a 1.3 to 1.5x reduction from WAL-based counting, produces a combined estimate in the range of 2,000 to 20,000 transactions per second. Against the measured 118 TPS baseline, that's a genuinely significant 15 to 150x improvement, nothing to dismiss. (Production has since grown to 500–1,000 TPS. Even from there, 500K is 500 to 1,000 times away.)</p>
<p>Measured against a 500,000 transaction per second target, even the optimistic end of that range falls roughly 25x short. That's the actual verdict: not reachable this way. Getting to 500,000 for real wouldn't mean tuning the current design harder. It would require a different architecture tier entirely: dozens to hundreds of shards rather than the 8 to 16 evaluated here, a move away from row-by-row ORM inserts with multiple dependent child-table writes per transaction toward bulk or columnar ingestion, since that per-transaction write pattern doesn't scale to that volume regardless of how many shards it's split across, and very likely a genuinely distributed database engine built for that scale, not a set of independently-sharded single-node Postgres instances glued together.</p>
<h2>Why saying no was the right call</h2>
<p>The easy version of this exercise ends with a roadmap slide showing a path to 500,000 TPS built out of optimistic rounding at each step. The honest version treats a target that large as an architecture-tier decision, a different system built differently from the ground up, not a tuning destination you inch toward by stacking incremental wins. The WAL-based counting work is still worth doing on its own terms, the hot-row contention it targets is real and worth fixing regardless. But it doesn't get oversold internally as a credible step toward a 500,000-tier system, because the math says plainly that it isn't one. Knowing the difference between a real, bounded improvement and a genuine step toward a stated target is most of what capacity planning actually is.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:54:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>capacity-planning</category>
      <category>systems-design</category>
      <category>postgresql</category>
    </item>
    <item>
      <title>From a Redis Relay to a Direct Feed: Rebuilding the Real-Time Pipeline</title>
      <link>https://insight.jatinjainsaraf.com/case-study/from-a-redis-relay-to-a-direct-feed-rebuilding-the-real-time-pipeline</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/from-a-redis-relay-to-a-direct-feed-rebuilding-the-real-time-pipeline</guid>
      <description>The old real-time pipeline worked. It also had a hop nobody needed anymore. Ripping it out across five repos took verified-dead traffic checks, a careful merge order, and knowing exactly which lookalike code path to leave alone.</description>
      <content:encoded><![CDATA[<h1>From a Redis Relay to a Direct Feed: Rebuilding the Real-Time Pipeline</h1>
<p>The old design for getting live block and transaction data onto SupraScan's frontend worked, and that's part of what makes it a useful case study. Nothing was broken. The indexer published every block and transaction to Redis pub/sub channels, a socket server subscribed to those channels and rebroadcast the data to connected browsers, and a second, entirely separate Redis stream fanned raw transaction data out to an external team's automated systems. It was a working relay with an extra hop, and the decision to remove that hop, across five separate repositories, is a good example of what disciplined deletion actually looks like.</p>
<pre><code>Old: an extra hop through Redis

  Chain Node --> Indexer --> Redis (pub/sub) --> Socket Server --> Browser
                                  |
                                  +--> Stream Redis --> External team

New: the socket server talks to the chain directly

  Chain Node &#x3C;==WS==> Socket Server --> Browser
                            |
                            +--> Redis (last 10 blocks / 20 txs, reconnect snapshot only)
</code></pre>
<p>The shape of the fix is visible in that comparison: the old path had the indexer sitting between the chain and the live feed even though the indexer's own job is writing to Postgres, not serving live updates. The new path removes it from that role entirely, and Redis goes from being the transport to being a small cache for one specific case, a client reconnecting mid-session.</p>
<h2>The new shape: a client, not a relay</h2>
<p>The replacement inverts the socket server's role. Instead of subscribing to an internal Redis channel that the indexer populates, the socket server itself becomes a WebSocket client, connecting directly to each blockchain node's own live feed for every environment it serves. The moment a message arrives from the node, it's broadcast straight to every connected browser, with no buffering interval and no per-tick cap holding messages back to smooth them out. Redis still plays a role, but a much smaller one: the last handful of blocks and transactions get cached there purely so a client that reconnects mid-session has something to show immediately, a snapshot, not the primary transport for live data anymore.</p>
<h2>Proving the old path was actually dead before deleting it</h2>
<p>The discipline here is in what happened before any code was removed, not in the replacement architecture itself. Before deleting the old pub/sub channels, a live subscriber count check against both the QA and production Redis instances confirmed those channels had zero active subscribers. That's a deliberate step, not an assumption: the team didn't reason their way to "nobody should be using this anymore," they checked, live, against production, and only then treated it as confirmed dead traffic. On top of that, the socket server's environment-enable configuration was flipped off for the two environments being migrated, on both QA and production, before the code that implemented the old path was even merged, specifically so there was no window where a live user could hit a code path that had already been half-removed.</p>
<p>The rollout itself crossed five separate repositories, and the merge order mattered: a shared library and the frontend went first, then the socket server, then the backend, then the indexer last. Each merge required a clean build and a grep-verified check that every deleted symbol genuinely had no remaining references anywhere in that repo, before moving on to the next one in the sequence.</p>
<h2>Scope that grew on purpose, and a landmine avoided</h2>
<p>One piece of functionality got folded into this same change deliberately, not accidentally: a service that computed live transactions-per-second by scanning a sliding window of recent blocks on every single block processed. It turned out to be more computationally expensive than either of the two systems that had already superseded it elsewhere in the stack, a lighter-weight live poll directly against the chain node's own metrics endpoint for the homepage display, and a completely separate metrics-accumulator pipeline feeding the analytics page. Removing a redundant, expensive computation while already touching this code was the right call, and it was made with the author's knowledge rather than discovered as scope creep after the fact.</p>
<p>One specific risk was flagged and handled explicitly during planning: the socket server's environment-enable configuration and its corresponding WebSocket routes had to be removed together, in the same change. Removing one without the other would either leave a live route pointing at handler code that no longer existed, or leave a stale configuration blocking routes that had already been deleted, both of which are the kind of half-finished cleanup that causes an outage days later when someone finally notices.</p>
<p>A second, opposite kind of care went into what wasn't removed. The indexer has its own internal publish and subscribe loop, structurally identical to the exact pattern being deleted everywhere else in this change. But this one is dual-purpose: the indexer's own main processing loop reads its current chain height from that same channel. Removing it under the assumption that it matched the pattern being cleaned up everywhere else would have broken block ingestion itself, not just an external, already-dead feed. It was explicitly flagged and kept, a reminder that the same-looking code in two places doesn't always mean the same thing.</p>
<h2>The number worth keeping</h2>
<p>A separate but related change in the same era replaced a cold-connect page load's dependency on a live RPC round-trip with a Redis-backed cache instead. The measured result: homepage cold-start hydration latency dropped from 60 seconds to 230 milliseconds. That's not a rounding improvement, it's a roughly 260x reduction in the worst-case time a new visitor waited before seeing live data, and it shipped as part of the same broader effort to make the real-time path leaner and less dependent on hops that weren't earning their cost.</p>
<h2>What was accepted, not fixed</h2>
<p>The new architecture isn't presented as flawless, and two specific limitations were knowingly accepted rather than solved. There's no per-client replay or acknowledgment: a client that disconnects and reconnects gets whatever the current cached snapshot happens to be, not a replay of exactly what it missed while offline. And the upstream connection from the socket server to each blockchain node has a roughly one-second reconnect gap, during which a block can genuinely be missed by that specific connection. Both are documented, known tradeoffs of favoring a simpler, more direct architecture over a more complete but heavier one, not oversights discovered later.</p>
<h2>What this demonstrates</h2>
<p>Deleting a working system safely is a different skill than building a new one. Every step here, checking live subscriber counts before removing channels, flipping configuration off before merging code, sequencing five repositories in a specific dependency order, and correctly distinguishing a truly dead code path from a structurally identical but load-bearing one, is about reducing the risk of the deletion itself, not about the destination architecture being clever. The destination here is genuinely simpler than what it replaced. Getting there without an outage was the actual work.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:53:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>websockets</category>
      <category>redis</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>One Codebase, Five Blockchain Environments</title>
      <link>https://insight.jatinjainsaraf.com/case-study/one-codebase-five-blockchain-environments</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/one-codebase-five-blockchain-environments</guid>
      <description>Mainnet, testnet, a MultiVM devnet, and two microchains all run the same indexer and backend code. A self-hosted infrastructure migration proved the abstraction actually holds, not just on paper.</description>
      <content:encoded><![CDATA[<h1>One Codebase, Five Blockchain Environments</h1>
<p>SupraScan runs against five separate blockchain environments today: production mainnet, production testnet, a MultiVM development network, and two independent microchains. All five run the exact same indexer codebase and are served by the exact same backend service. The interesting engineering question isn't "how do you support five environments," it's "how do you add a sixth without touching the code that already works for the first five," and the answer to that turned out to hold up under a genuinely different kind of test than the one it was designed for.</p>
<h2>The indexer scales out, the backend scales in</h2>
<p>The indexer side of this is deliberately simple: a single codebase, with a configuration variable selecting which blockchain it's pointed at, deployed as a separate running instance per environment. Each deployed instance polls its own RPC endpoint and writes to its own dedicated database. There's no cross-environment coordination at the indexer layer at all, by design; five environments means five independent processes running the same code against five different inputs.</p>
<p>The backend inverts that shape entirely. Instead of five separate backend deployments, one Apollo Federation service serves every environment at once, specifically so a single deployment can serve all of them simultaneously rather than multiplying the number of running backend processes by the number of environments. Every incoming GraphQL request carries a field identifying which environment it's asking about, and a connection manager routes the query to the correct database pool based on that field. The routing isn't a simple two-way split, though, and it's worth being precise about which environments actually share infrastructure today rather than assuming the obvious grouping: mainnet gets its own dedicated pool, one specific microchain has since been given its own dedicated pool as well, and testnet, the MultiVM devnet, and the other microchain share one pool. That's a narrower, more deliberately tiered split than "mainnet alone, everyone else together," and which environments get pulled out of the shared pool has evidently changed at least once since this system was first documented, this microchain's dedicated pool is a more recent addition than the routing scheme's original design.</p>
<p>That drift is worth naming directly: the system's own written documentation still describes the older, simpler grouping, mainnet on one side and testnet, multivm, and every microchain sharing the other, while the actual routing code has since moved one specific environment out of the shared pool without the documentation catching up to reflect it. It's a small, ordinary kind of staleness, the code evolved and the README didn't, but it's a genuine example of why checking live behavior matters even when a design doc already exists and looks authoritative.</p>
<p>There's still a real tension worth sitting with in the environments that do share a pool. A slow query or a resource spike on one environment's traffic runs through the exact same backend process serving every other environment sharing that pool. That's the opposite tradeoff from the one made on the Redis side of this same system, where the two networks judged to need isolation, mainnet and that same promoted microchain, got their own dedicated Redis instances specifically to stop one environment's cache behavior from contaminating another's, while the rest still share a default instance. The database-pool split and the Redis-instance split aren't drawn along quite the same lines, but they're answering the same underlying question, which environments earn dedicated infrastructure and which don't, and the fact that both keep getting revisited as specific environments grow suggests the current split, in either layer, isn't meant to be permanent.</p>
<p>Adding a genuinely new environment to this system is a documented, five-step procedure: register the new environment in a reference table, add its connection configuration to the connection manager in both the indexer and backend codebases, add its RPC endpoint configuration, add a Redis key namespace for it, and deploy a new indexer instance pointed at it. None of those five steps require touching the core indexing or query logic. That's the actual measure of whether an abstraction like this is real: can you add a sixth environment without anyone needing to reason about the first five.</p>
<h2>The test this abstraction didn't expect: a different kind of infrastructure entirely</h2>
<p>Most of the time, "add a new environment" means the same kind of managed cloud database on a new connection string. A more interesting test came from a different direction: two of the microchain-tier databases were migrated onto plain, self-hosted PostgreSQL running directly on virtual machines, not a managed database service, and not a Kubernetes-orchestrated cluster like some of the other environments use. That's a genuinely different infrastructure category from the managed cloud database clusters the rest of the system runs on, and it's exactly the kind of change that exposes an abstraction that only looked clean on paper.</p>
<p>It held. A direct schema comparison against the equivalent managed database cluster, run live, confirmed 290 tables on the self-hosted box, including the full set of already-created daily partitions, matching the production structure exactly, and a byte-for-byte diff of table and index names between the self-hosted box and its managed counterpart came back identical across all 819 index names. The same application code connected to either backing database correctly, with the only difference being a connection string. Both self-hosted databases also came up with zero database triggers, consistent with the trigger-removal work described elsewhere in this series: any new environment stood up after that work inherits application-level counting logic from day one, with nothing left to migrate. Every environment checked directly, production and QA, mainnet and testnet alike, showed the same result for the two trigger-removal passes confirmed shipped; one further trigger removal from that same series was still awaiting independent confirmation of its production rollout as of this writing, so 'no triggers anywhere' holds for what's been re-verified live, not yet as a fully closed book.</p>
<h2>What this demonstrates</h2>
<p>A multi-environment architecture is easy to design well when every environment looks the same underneath. This one got tested by an environment that didn't look the same underneath at all, a different hosting model entirely, and the abstraction held without a single code change. That's a more honest test of the design than adding another environment that looks just like the last one, and it's the kind of validation that only shows up when infrastructure decisions made for unrelated reasons, in this case a self-hosting migration on cost or operational grounds, happen to stress-test an architectural boundary nobody built specifically to be tested that way.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:52:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>architecture</category>
      <category>multi-environment</category>
      <category>postgresql</category>
    </item>
    <item>
      <title>Paying Down Three Years of JSONB Debt</title>
      <link>https://insight.jatinjainsaraf.com/case-study/paying-down-three-years-of-jsonb-debt</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/paying-down-three-years-of-jsonb-debt</guid>
      <description>JSONB is the right tool for a blockchain&apos;s variable-shaped data, until it isn&apos;t. A tiered removal plan, twelve columns re-classified one by one, and three columns nobody&apos;s allowed to touch because another team reads them directly.</description>
      <content:encoded><![CDATA[<h1>Paying Down Three Years of JSONB Debt</h1>
<p>JSONB earns its place in a blockchain indexer's schema honestly. Move-based chains produce resources and event payloads with genuinely variable shape, and modeling every one of those shapes as its own narrow table would mean either a schema with hundreds of tables or an unmaintainable entity-attribute-value design. JSONB is the pragmatic answer to that problem. The debt shows up later, once you've been writing JSONB columns for three years and nobody's gone back to ask which of them still earn their keep.</p>
<h2>Twelve columns, and the ones nobody was reading</h2>
<p>A systematic pass through the schema classified twelve JSONB fields across the indexer's tables into removal phases, ranked by how safe and valuable removal would be. The first tier, already shipped, targeted the clearest cases: a column storing execution statistics on the block table that turned out to be fully derivable from two typed columns already sitting right next to it. A metadata column on the fungible-asset table that nothing in the backend ever actually read, replaced by fetching the same data from the chain's RPC layer on demand instead of storing it at all. A column on an automation gas-assessment table that had been storing an entire event payload when only one derived number from it was ever used anywhere, that number now extracted in memory instead. A signature column on the transaction detail table, dropped the same way, because nothing downstream depended on it being persisted. The rollout for each of these went through an intermediate step before the column disappeared entirely, writes were stopped first, reducing the column to an empty placeholder while everything downstream adjusted, and only then was the column itself dropped. Checking the live schema today confirms all four have completed that full arc: none of them, execution statistics, the fungible-asset metadata field, the gas-assessment payload, or the transaction signature column, exist anywhere in the current tables anymore, not even as an unused placeholder.</p>
<p>None of these were large individually. The observed savings on a lower-volume environment were around 10 megabytes a day, which sounds unremarkable until you scale it by production's substantially higher write volume and multiply by however many years the column would otherwise have kept accumulating. The backend service that reads from these same underlying tables had its own, independent version of this cleanup running in parallel, removing the same category of unused JSONB columns from its side of the codebase, because each service has its own read and write paths into the same tables and each needed its own audit.</p>
<h2>The three columns nobody gets to unilaterally drop</h2>
<p>Five more fields were still being classified into later removal phases at the time of this writing, with a further idea under evaluation: for the columns that genuinely need to stay, converting the storage format itself to something more compact than raw JSONB, to shrink the cost of keeping them without removing them outright.</p>
<p>Three specific columns sit in a different category entirely, and they're also the largest storage contributors in the entire schema: a payload field on the transaction detail table, an authenticator field on the core transaction table, and a content field on the event table. These three are read directly by teams outside SupraScan's own codebase, which means dropping or reshaping them isn't a decision this team gets to make alone, regardless of how much storage they cost. The options being weighed for these three are all about reducing their footprint without breaking that external contract: compressing the data at write time into a binary column instead of raw JSONB text, moving them into a separate, colder table linked by a foreign key so the frequently-queried hot tables stay lean, or working directly with the teams that depend on them to see whether an API could replace the need for a direct database read at all. None of those are simple swaps, and none of them are happening unilaterally.</p>
<h2>The rule that exists now because this happened</h2>
<p>The lasting output of this work isn't any single column removal. It's a standing rule, written down rather than left as tribal knowledge, that gets applied before any new JSONB column is added to this schema: can this be typed columns instead, can this be fetched from the chain's RPC layer on demand rather than stored at all, and is this something that's only ever written and never actually read, in which case it shouldn't be stored in the first place. Three years of JSONB columns accumulated because each individual one seemed like a reasonable, low-friction choice at the time it was added. The rule exists specifically so the next three years don't repeat the same accumulation for the same reasons.</p>
<h2>What this demonstrates</h2>
<p>The interesting tension in this story isn't "JSONB is bad." It's that a tool chosen correctly for a real problem, modeling genuinely variable-shaped blockchain data, still accumulates debt if nobody revisits whether each individual use of it is still earning its cost. Some of these columns had already stopped being read years before anyone checked. Others are permanently protected not because they're well designed, but because an external dependency makes them expensive to change regardless of their internal cost. Paying this down wasn't one migration. It was a classification exercise, a standing rule to prevent recurrence, and an honest acknowledgment that some of the debt isn't fully payable without a conversation that doesn't belong to this team alone.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:51:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>postgresql</category>
      <category>jsonb</category>
      <category>storage-optimization</category>
    </item>
    <item>
      <title>Concurrency Design Under High TPS: Moving Wallet Counters Off Database Triggers</title>
      <link>https://insight.jatinjainsaraf.com/case-study/concurrency-design-under-high-tps-moving-wallet-counters-off-database-triggers</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/concurrency-design-under-high-tps-moving-wallet-counters-off-database-triggers</guid>
      <description>A 3-4 day indexer lag traced back to four wallets and a database trigger. The fix moved counting logic out of the database entirely, and a routine check-with-the-docs step turned up an architectural assumption that had been wrong all along.</description>
      <content:encoded><![CDATA[<h1>Concurrency Design Under High TPS: Moving Wallet Counters Off Database Triggers</h1>
<p>Four automation wallets, each targeted by roughly 500 automation tasks landing in the same block, was enough to stall the indexer for three to four days. That's the incident that forced a redesign of how SupraScan counts wallet transaction totals, and the redesign that followed it is a good example of how a fix motivated by one incident ends up exposing a second problem nobody was originally looking for.</p>
<h2>What the trigger-based design got wrong under load</h2>
<p>Wallet transaction counts were originally maintained by database triggers: insert a transaction row referencing a wallet, and a trigger on that table incremented the wallet's stored count automatically. It's a clean pattern in isolation, and it works fine until the same wallet row is targeted by many concurrent writers at once. When automation tasks fire in bulk against a small number of wallets in the same block, every one of those inserts tries to lock and update the same handful of wallet rows through the trigger, and the database has to serialize all of that contention. Four wallets absorbing roughly 2,000 automation-triggered transactions in a single block was enough to create a multi-day backlog, because the trigger contention on those specific rows became the bottleneck for the entire indexing pipeline, not just for those four wallets.</p>
<p>The fix for the insert side moved wallet-count maintenance out of the database trigger and into application-level logic, giving the code direct control over how and when those counts get updated, instead of leaving it to a mechanism that serializes on row locks by default.</p>
<h2>The mirror work on delete, and the review that caught what testing didn't</h2>
<p>Fixing the insert side solved the immediate incident, but the same tables also had delete-side triggers doing the equivalent decrement, and those carried the identical contention risk. The mirror work replaced three separate delete triggers, on the sender, receiver, and fee-payer tables, with application-level decrement logic, following the same pattern already proven on the insert side.</p>
<p>What's worth calling out here isn't the mechanism, it's the process around shipping it. Three real correctness bugs were found and fixed during code review, before merge, and two of those three were caught by a reviewer, not by the person who wrote the change. That's a detail easy to leave out of a case study because it isn't flattering in the usual sense, but it's exactly the kind of evidence that matters: a change this close to core data integrity got a second set of eyes that found problems the first pass missed, and the process worked the way it's supposed to.</p>
<p>The rollout itself was deliberately cautious given what was at stake. Processing was fully paused, a drift query ran against the still-paused system comparing the stored wallet totals against a value freshly recomputed from the source transaction tables, the trigger-removal migration ran, the same drift query ran again to confirm nothing had changed, and only then did processing resume. QA testnet went first. The full rollout, across every QA environment and both production networks, shipped in early August 2026, with the triggers confirmed dropped and the drift check clean on every environment.</p>
<p>A separate, unrelated bug turned up during this same work: a configuration flag governing in-loop transaction-info removal was found live in production, in a way that violated its own documented pairing rule with the partition-management flag from the storage-scaling work. It was flagged, not fixed immediately, a reasonable call given it wasn't the thing actively causing an incident.</p>
<h2>The bigger question this raised, and the assumption that turned out to be wrong</h2>
<p>While this work was underway, a separate initiative was considering something much larger: building a full write-ahead-log-based counting system to replace the interim application-level fix entirely, with its own dedicated architecture and integration specs. The stated motivation wasn't the load already observed, it was a theoretical worst-case burst ceiling, on the order of 500,000 transactions per second under a maximum block rate and block size, versus a sustained load at the time closer to six transactions per second. The concern was hot-wallet row contention under a burst nowhere near what had actually happened yet.</p>
<p>Midway through that discussion, a direct check against the live production deployment configuration turned up something that changed the shape of the entire argument: the production indexer was running as a single instance, not the two instances that existing architecture documentation had claimed. That distinction matters enormously for a concurrency argument. A single instance can coordinate its own internal work without needing distributed locking, because there's no second process to race against. The primary justification for building a distributed, Redis-coordinated write-ahead-log system, protecting against cross-instance concurrent writers, simply didn't apply to the system as it was actually deployed. What remained of the WAL system's value after that correction was observability and per-module statistics, useful, but a different and much smaller problem than the one it was originally scoped to solve.</p>
<h2>Two more trigger removals, and one that both prior efforts missed</h2>
<p>The same interim pattern, application-level counting instead of a database trigger, was later mirrored for automation execution counts on a related table, shipped to QA first with production following shortly after. A third trigger, tied to automation registration counts on the same underlying entity, was found only later, and only because of a full, systematic audit across every live database rather than trusting that two prior rounds of "complete" migration had actually been complete. Two separate migration efforts, each believed finished when it shipped, had both missed a related trigger sitting on the same table family. The audit that finally caught it didn't rely on anyone remembering to check; it re-verified live state directly, which is the only thing that actually caught what two rounds of careful, reviewed work had both missed. Two of those three migration passes, the insert-side and delete-side removals on the sender, receiver, and fee-payer tables, are confirmed shipped and verified clean across every environment. The third, the registration-count trigger this same audit surfaced, was implemented in application code but its merge and production rollout weren't independently reconfirmed as of this writing, worth stating plainly rather than rounding up to 'done' before the same kind of live check that caught it in the first place gets run against it again.</p>
<h2>Why this mattered beyond the indexer's own stability</h2>
<p>This work wasn't prioritized purely for the indexer's own sake. A separate team building automation tooling had privately validated their own system could handle 100,000 concurrent tasks on a private test network, but had explicitly agreed not to raise the live, on-chain task-capacity limits on the real network until the indexer's concurrency handling was confirmed ready for that scale. The chain's own configuration allows those capacity limits to be raised with no hard ceiling in the underlying code, only a governance action, which meant the actual blocker to a real capacity increase wasn't a protocol limit, it was confidence that the indexer wouldn't repeat the four-wallet incident at ten or a hundred times the concurrent load. This trigger-removal and concurrency-hardening work was the literal, agreed-upon gate that team was waiting on, not preparation for a hypothetical future.</p>
<h2>What this demonstrates</h2>
<p>The most useful thing about this sequence isn't the trigger-to-application-logic pattern itself, which is a known technique. It's what surrounded it: an incident that motivated an immediate fix, a review process that caught real bugs a test suite hadn't, a live-configuration check that overturned an architectural assumption a much larger initiative had been built on, and an audit discipline that treated a prior migration's own claim of completeness as something to verify, not trust. Concurrency bugs at this scale rarely announce themselves cleanly. Finding them consistently is less about any one clever fix and more about refusing to assume the last fix, or the existing documentation, already got it right.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:50:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>postgresql</category>
      <category>concurrency</category>
      <category>architecture</category>
      <category>distributed-systems</category>
    </item>
    <item>
      <title>Exact Analytics in 12KB: Hardening a Wallet-Uniqueness System</title>
      <link>https://insight.jatinjainsaraf.com/case-study/exact-analytics-in-12kb-hardening-a-wallet-uniqueness-system</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/exact-analytics-in-12kb-hardening-a-wallet-uniqueness-system</guid>
      <description>HyperLogLog gets you exact distinct-wallet counts at near-zero memory cost. The hard part is not the algorithm, it&apos;s keeping it correct under concurrent production writes. Five real bugs, one week, five different ways correctness quietly slips.</description>
      <content:encoded><![CDATA[<h1>Exact Analytics in 12KB: Hardening a Wallet-Uniqueness System</h1>
<p>How many distinct wallets touched the network in the last hour. It sounds like a question with an easy answer until you try to answer it at scale, continuously, across five blockchain environments, without storing every wallet address that was ever active in every window you care about. SupraScan answers it with HyperLogLog sketches: a fixed, tiny amount of memory per time bucket gives an exact distinct-wallet count for any window you ask about, five minutes, an hour, a day, without the storage cost of ever keeping the underlying address list around. That mechanism working reliably in production, continuously, is the achievement. The part worth writing about in detail is what it actually took to get there: in one specific week, the system's reported numbers were wrong five separate times, for five genuinely different reasons, and finding each one is as much a part of "hardening a production system" as the sketch design itself.</p>
<h2>Why sum isn't the same as union</h2>
<p>The core mechanism is a sketch union: instead of storing every distinct wallet address seen in a time bucket, the system stores a small, fixed-size sketch that can answer "roughly how many distinct items have I seen" using a fraction of the memory a full set would need. The system counts unique senders and receivers this way, and it keeps a sketch for every 5-minute bucket, then needs longer windows, an hour, a day, built from those. The reason those longer windows can't just add up their constituent buckets' counts is structural, not a rounding preference: a wallet that shows up in more than one 5-minute bucket within the hour is a single wallet, and adding the per-bucket counts together counts it once for every bucket it happened to appear in, while simply taking the largest of the per-bucket counts undercounts everything down to whichever single bucket happened to be biggest. A union of the sketches themselves is the only operation that gives the actual right answer for the combined window, which is exactly why the system keeps merging 5-minute sketches upward into longer-lived hourly and daily ones rather than trying to reconstruct a rollup from already-computed counts.</p>
<p>That distinction is not academic. If bucket A has 55 unique senders and bucket B has 2 unique receivers, and some of those addresses appear in both, summing the two counts overcounts by exactly the overlap. Unioning the sketches gives the true combined count, 59 in that specific case, because a union correctly recognizes overlap the way a sum structurally cannot. The first fix in this saga replaced a cardinality sum with a true sketch union, paired with a corresponding change on the backend that consumes the corrected sketches. It's the fix that makes the system exact instead of approximately-too-high, and it's also the fix that makes everything downstream sensitive to a category of bug that a naive counter would never have exposed in the first place.</p>
<h2>Five bugs, five different ways to catch them</h2>
<p>Once the union fix was in place, four more real correctness bugs surfaced and were fixed over the following days, each caught by a completely different signal.</p>
<p>The second was a double-counting bug on reseal: an operation meant to update a cumulative counter was using simple addition instead of a "take the greater of the two values" comparison, so re-sealing a bucket that had already been sealed once doubled its stored total. This one was caught by an internal invariant checking itself: a 5-minute bucket showed more unique senders than the full hour that was supposed to contain it, which is structurally impossible if the counting is correct, and that impossibility is what surfaced the bug.</p>
<p>The third bundled two separate issues in one commit. The sender-count metric was reading from the wrong source column, quietly undercounting. Separately, a get-then-increment pattern across concurrent block-processing pods created a race window that could inflate peak metrics like maximum TPS or maximum gas price, because two pods could both read the same "current maximum" value before either had written back their update. The fix for the race was to replace the separate read and write with a single atomic compare-and-set, implemented as a Lua script, closing the window entirely rather than trying to make the race smaller. The shape of that fix, illustrative of the pattern rather than a verbatim excerpt, looks like this:</p>
<pre><code class="language-lua">-- KEYS[1] = the peak-metric key for one environment (e.g. a maximum-throughput counter)
-- ARGV[1] = the newly observed value from this pod
local current = tonumber(redis.call('GET', KEYS[1]) or '0')
local candidate = tonumber(ARGV[1])
if candidate > current then
  redis.call('SET', KEYS[1], candidate)
  return candidate
end
return current
</code></pre>
<p>The point of running this as a single script isn't the comparison itself, it's that Redis executes a Lua script atomically. Two pods can call this at the same instant and still only one write happens for whichever value is actually higher, because there's no gap between the read and the write for a second caller to land in. A plain <code>GET</code> followed by a separate <code>SET</code> from application code always has that gap, no matter how small it looks in a benchmark.</p>
<p>The fourth was the least intuitive of the five. A bulk-insert call configured to ignore duplicate rows was, by the specific behavior of the ORM version in use, returning every input row regardless of which ones were actually new inserts versus duplicates silently skipped by the underlying conflict-do-nothing clause. That meant a "new wallets" counter had been reporting batch size, not actual new-wallet count, for an unknown period of time, and it stayed undetected until someone traced the number against ground truth: the same insert statement's own returning clause, which only reports rows that were genuinely inserted. Once that comparison was made, the gap was obvious. Before it, the metric looked plausible on every dashboard that displayed it.</p>
<p>The fifth, and most recent, was a data-quality bug rather than a counting-logic bug: event parsers were hardcoding a transaction's origin type to "user" regardless of what the transaction's actual origin was, which inflated sender counts specifically on digital-asset and NFT-related rows that weren't actually user-originated. It shipped alongside a cleanup that removed redundant, duplicate HLL calls sitting in the same code path.</p>
<h2>The pipeline underneath, and the incidents it produced on its own</h2>
<p>The counting mechanism sits inside a small pipeline of its own: a service increments Redis-backed counters per environment as events happen, and a leader-elected "sealing" process periodically flushes completed time buckets into a durable table. Two separate incidents came out of that pipeline, independent of the five counting bugs above.</p>
<p>The first: the leadership-election check for who becomes the sealing leader only ran once, at process startup. A quick pod restart left a stale leadership heartbeat behind, and no pod ever re-ran the election to notice the old leader was gone. Sealing stalled for roughly 29 hours on one environment before a later restart happened to trigger a fresh election and catch everything up. No data was actually lost, because the underlying Redis counters kept accumulating with their TTLs refreshed the whole time, but the reported metrics were stale for over a day before anyone would have seen it in a dashboard. The fix was straightforward once diagnosed: retry the leadership election on every seal cycle, not only once at startup.</p>
<p>The second was a load problem, not a data problem. During any catch-up or reprocessing run, every block being reprocessed carries a historical, not live, timestamp. That historical timestamp routed metric increments down a direct-to-database write path instead of the normal Redis-then-leader-seals path, and with many pods reprocessing concurrently, that meant hundreds of simultaneous inserts competing for the same handful of bucket rows, with most of them stuck lock-waiting. The practical effect was block ingestion throughput dropping to zero every time this write path triggered during a large catch-up. The workaround in place is to disable that direct-write path entirely during any catch-up or reprocessing run. A proper fix, moving those historical writes through the same leader-only accumulation path as live traffic, hadn't shipped as of this writing.</p>
<h2>What this actually demonstrates</h2>
<p>The algorithm here is not the hard part, and pretending otherwise would undersell what actually happened. HyperLogLog union is a known, well-documented technique. What's harder, and less often written about, is that a sketch-based counting system sits at the intersection of several failure modes that don't show up in a tutorial: reseal semantics, concurrent-writer races, ORM behavior that silently changes what a "successful insert" means, and a leader-election scheme that has to keep re-checking itself rather than assuming its first decision holds forever. Five real bugs in one week isn't a sign the system was poorly built. It's what building something that reports an exact number, under continuous concurrent load, from a fundamentally approximate data structure, actually looks like when you take correctness seriously enough to keep checking it.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 06:49:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>hyperloglog</category>
      <category>redis</category>
      <category>analytics</category>
      <category>distributed-systems</category>
    </item>
    <item>
      <title>Scaling a Blockchain Database From a Single Partition to 4.6 TB</title>
      <link>https://insight.jatinjainsaraf.com/case-study/scaling-a-blockchain-database-from-a-single-partition-to-4-6tb</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/scaling-a-blockchain-database-from-a-single-partition-to-4-6tb</guid>
      <description>A partition strategy that worked in 2021 became a 10TB liability by 2026. Here&apos;s the five-stage evolution that got it back under control, the outage the last stage caused, and the storage arc from 10TB avoided to 4.6TB today.</description>
      <content:encoded><![CDATA[<h1>Scaling a Blockchain Database From a Single Partition to 4.6 TB</h1>
<p>In 2021, SupraScan's transaction tables lived in a single partition each. That was the right call at the time: low volume, simple queries, nothing to gain from splitting data you could scan end to end in milliseconds anyway. Five years later, that same table shape would have been an operational disaster. The interesting part of this story isn't that partitioning strategy changed. It's that it changed five separate times, each change a direct response to a problem the previous grain created, and the last of those changes caused a real production outage before it was fully safe.</p>
<h2>Stage one: one partition, then six months, then a monster</h2>
<p>The grain tightened in stages as ingestion grew. From 2021 through mid-2024, everything sat in one partition per table. From mid-2024 through the start of 2025, tables were split into six-month partitions, still coarse, but enough to keep individual partitions from growing unbounded.</p>
<p>Then came the full-year partition covering all of 2025, created when the six-month grain was tightened again. That single partition, covering an entire year of transaction events, became the largest structural liability in the entire database. By the time a dedicated storage audit looked at it in April 2026, the prod SupraScan database sat at 10 TB on a single Postgres instance, and that one year-long event partition alone accounted for 4.9 TB of that, roughly half the entire database, driven by three JSONB columns storing content, event data, and identifiers. A separate transaction-detail table was carrying 71 to 86 percent index overhead, meaning its indexes were two to three times larger than the data they indexed. Ingestion at that point was running around 500,000 transactions and 11 million events per day, all still landing in yearly buckets.</p>
<h2>The audit, and the decision not to rebuild everything at once</h2>
<p>Faced with a 10 TB database and a monster partition eating half of it, the natural instinct is to reach for a bigger architectural change. Three tiering options were seriously evaluated, and it's worth being honest about how each one would actually have played out, not just what it promised on paper. Moving cold data into ClickHouse for the older window would have delivered the best compression and the fastest historical queries of the three, at the real cost of operating a second database engine indefinitely: a new sync pipeline, a second system to monitor, and a second place a query result could quietly drift from the source of truth. Archiving cold data to object storage as Parquet was the cheapest option by a wide margin, but it pushed historical query latency out to 2 to 10 seconds, a real, felt tradeoff for anyone or anything querying older data, not a rounding error. Staying entirely inside Postgres with TimescaleDB's columnar compression was the lowest-effort option precisely because it didn't introduce a second engine at all, at the cost of a compression ceiling lower than a purpose-built columnar store would give.</p>
<p>None of those decisions were made in isolation from a more basic fact: regardless of which tiering approach eventually gets picked, a set of quick wins didn't depend on the choice at all. Extracting frequently-queried JSONB fields into typed columns, auditing and dropping unused indexes on that same transaction-detail table, splitting the year-long monster partition into monthly chunks, and adding Redis caching for hot-path queries were all identified as work worth doing regardless of the bigger architectural direction. That's the pattern worth naming here: the team didn't wait for a strategic decision to start paying down the parts of the debt that were unambiguous.</p>
<h2>Tightening the grain again, and the outage that came with it</h2>
<p>The partition grain kept tightening after the initial audit: monthly partitions from early 2026, then daily partitions starting in April 2026, auto-created seven days ahead of when the data would land, managed by a dedicated partition-management service running on UTC midnight. The reasoning for going all the way to daily wasn't purely about the production database. QA and testnet environments only need to retain seven days of data, and dropping a daily partition is instant, where deleting the equivalent rows row-by-row was generating real database load on environments that didn't need the data kept at all. Production mainnet runs in create-only mode, keeping every partition forever; QA and the testnet tiers run with cleanup enabled, dropping partitions older than the retention window automatically.</p>
<p>This is also where the evolution produced a real incident. On April 27 and 28, 2026, the automated partition-creation cron silently failed to create the upcoming daily partitions for exactly three tables: the sender, receiver, and fee-payer tables, the only partitioned parents in the schema carrying <code>AFTER DELETE</code> row-level triggers. The reason was subtle: creating a partition on a table with active triggers loses a lock race against the live indexer process writing to that same table, and the create-only rollout hadn't built in a pause for that window. The failures were caught, but only logged at warning level per table, so the automated job looked healthy from the outside while it was quietly failing on exactly the tables that mattered most. The outage hit at 03:57 UTC on April 29, when the indexer tried to insert into a partition that had never been created. It was fixed manually with a defensive <code>CREATE TABLE IF NOT EXISTS ... PARTITION OF</code> statement, and two concrete follow-ups came out of it: pausing the indexer during partition creation, and extending the creation lookahead from two days to seven, so a single missed run has more runway before it becomes a live outage.</p>
<h2>Where the numbers landed</h2>
<p>A live measurement taken shortly after this work, on May 4, 2026, put the production mainnet database at 4,667 GB, roughly 4.6 TB, down from the 10 TB the April audit had measured on the same instance. It's worth being honest about what does and doesn't explain that drop: the individually-quantified levers, 229 GB from the index audit and roughly 10 MB a day from the first JSONB removals, don't add up to 5.4 TB on their own. The rest is best attributed to the broader restructuring work described here, not to a single measured fix, and that gap is worth naming rather than implying away. The old monster partition's damage was still visible in the numbers even after the paydown: that same year-long transaction-detail partition alone held 452.9 million rows across 1,641 GB, 86 percent of which was index, not data. The new daily partitions for 2026 were running at a much more modest 4.5 to 5 GB per day.</p>
<p>The same audit pass surfaced a second, entirely separate lever: a systematic scan for indexes that had never been scanned, ever, in production. It found 229 GB across 296 zero-scan indexes on prod mainnet alone, with individual offenders as large as a 57 GB primary key index on an automation-fees table with zero recorded scans. That number, sitting right next to the partition work, is the honest reminder that storage debt in a system like this rarely has one cause. Five years of grain changes, a JSONB-heavy schema, and indexes created for a query pattern that never materialized all compound in the same database at the same time, and the fix for each one looks nothing like the fix for the others.</p>
<p>The grain history is still visible directly in the schema today, not just in a report from the time it happened: the earliest partition on the core transaction tables still covers that original 2021-through-2024 span, followed by the six-month partition marking the next grain change, the full 2025 partition still carrying the size of the monster year it covers, three separate monthly partitions for the first quarter of 2026, and daily partitions from spring 2026 onward, currently running several days ahead of the present date exactly as a seven-day creation lookahead would produce. Fifteen separate tables carry this same partitioning strategy today. The evolution this piece describes isn't a closed chapter being reconstructed from memory, it's sitting in the live schema as a literal, dated record of every decision described above.</p>
<h2>What the progression actually shows</h2>
<p>None of the five grain changes were wrong at the time they were made. Yearly partitions were a reasonable tightening of six-month partitions, until ingestion volume and JSONB column size turned that year into a 4.9 TB single object. What makes this a case study rather than a postmortem is that each stage's cost only became visible under the load the previous stage couldn't have anticipated, and the team's response each time was to tighten the grain further rather than declare the whole model broken and start over. The daily-partition outage is part of that same story, not a separate failure. It's what happens when a mature, working pattern gets pushed one more notch tighter and meets a database-level detail, trigger locking during DDL, that the earlier, coarser grains had never been fast-moving enough to expose.</p>]]></content:encoded>
      <pubDate>Tue, 01 Sep 2026 05:43:24 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>postgresql</category>
      <category>partitioning</category>
      <category>storage-optimization</category>
      <category>scaling</category>
    </item>
    <item>
      <title>The Dual-Write Problem: Why &quot;Update the DB, Then Publish an Event&quot; Is Broken</title>
      <link>https://insight.jatinjainsaraf.com/the-dual-write-problem-outbox-sagas-and-2pc</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-dual-write-problem-outbox-sagas-and-2pc</guid>
      <description>An order gets saved to Postgres. The event that&apos;s supposed to tell payments, shipping, and notifications never goes out, because the process crashed one line later. Here&apos;s why that gap exists, and the two real patterns (Outbox, Sagas) that close it.</description>
      <content:encoded><![CDATA[<p>An order gets saved to Postgres. The next line of code publishes an "order created" event to a message broker, so the payments service can charge the customer, shipping can start prepping a label, and notifications can send a confirmation email. That's the whole design. It works in every test, in staging, and in production, for months.</p>
<p>Then one day the process gets killed between those two lines. Deploy rolled a pod mid-request. The broker connection timed out for four seconds during a network blip. A GC pause pushed the request past a load balancer timeout and the client retried against a different instance while the first one was still finishing up. The order is sitting in the database, fully committed, real, billable. Nobody downstream ever hears about it. No exception was thrown. No alert fired. The system looks healthy. A customer just placed an order that will never ship.</p>
<hr>
<h3>Why You Can't Just Wrap It in a Transaction</h3>
<p>The instinct is to reach for a transaction: begin, insert the order, publish the event, commit. That doesn't work, and it's worth being precise about why, because the reason is architectural, not a missing try/catch.</p>
<p>A database transaction gives you atomicity over one resource: the database. Postgres can guarantee the order row either exists or doesn't, with nothing in between visible to other readers. A message broker (Kafka, RabbitMQ, SQS) is a <em>separate system</em> with its own commit protocol, its own durability guarantees, and no shared transaction coordinator with your database. There's no operation that atomically says "commit this Postgres row and this Kafka message together, or neither." You are making two independent network calls to two independent systems, and between them there is always a window where one has succeeded and the other hasn't.</p>
<p>Move the event publish before the database commit and you get the opposite failure: the event goes out, downstream services start acting on an order that doesn't exist yet, and then the database write fails (constraint violation, connection drop, whatever) and now payments has charged a card for an order that was never actually created. Order doesn't matter. Two independent writes to two independent systems, with no atomicity across the boundary, will eventually diverge no matter which one goes first. This is the dual-write problem, and it's not a bug in your code, it's a gap in what a database transaction can promise you.</p>
<p><strong>Why this matters more than it looks like it should:</strong> this isn't a rare edge case you can accept as background noise. Every deploy is a process restart. Every autoscaling event is new pods coming up while old ones drain mid-request. Every network blip between your service and your broker is a window for this to happen. At low traffic it might occur once a month and get written off as "weird, must've been a fluke." At real scale, with thousands of writes a minute, this gap fires constantly, and it fires exactly during the conditions you're least equipped to notice it: deploys and incidents, when your attention is already somewhere else.</p>
<hr>
<h3>Two-Phase Commit: The Theoretically Correct Answer Nobody Uses</h3>
<p>The textbook fix for "atomically commit across two systems" is Two-Phase Commit (2PC). A coordinator asks every participant (the database, the broker) to prepare, meaning "lock this resource and confirm you <em>can</em> commit, but don't commit yet." Once every participant says yes, the coordinator tells everyone to commit for real. If any participant says no, the coordinator tells everyone to roll back.</p>
<p>It's a real protocol with a real correctness proof, and it's almost never what production systems actually use for this problem, for reasons that show up the moment you operate it instead of just reading about it:</p>
<ul>
<li><strong>It's blocking.</strong> Once a participant says "yes, I can commit," it has to hold that lock until the coordinator's final decision arrives. If the coordinator crashes after collecting votes but before sending the commit decision, every participant is stuck holding locks indefinitely, waiting for a coordinator that might not come back.</li>
<li><strong>Most message brokers don't implement the participant side of the protocol at all.</strong> Kafka has no native 2PC participant role. You'd be building and maintaining that coordination layer yourself, on top of a system that was never designed to expose it.</li>
<li><strong>It doesn't survive network partitions gracefully.</strong> The entire protocol assumes the coordinator can eventually reach every participant. A partition during the decision phase leaves things in exactly the indeterminate, locked state the protocol was supposed to prevent.</li>
</ul>
<p>2PC is the right answer to a narrower question: coordinating multiple <em>databases</em> that all speak a compatible protocol, in a controlled environment where you own the failure modes. It's the wrong tool for "coordinate my database with my message broker," which is the shape of the dual-write problem almost everyone actually hits.</p>
<hr>
<h3>The Outbox Pattern: Make the Second Write Boring</h3>
<p>The pattern that actually gets used starts from a reframe: stop trying to make two different systems commit atomically, and instead make the <em>second write</em> something the <em>same</em> database transaction can own.</p>
<p>Instead of publishing to the broker directly, you write the event as a row in an <code>outbox</code> table, in the exact same transaction as the order insert:</p>
<pre><code class="language-sql">BEGIN;
INSERT INTO orders (id, customer_id, total_cents, status)
  VALUES ('ord_123', 'cust_456', 4999, 'created');
INSERT INTO outbox (id, aggregate_id, event_type, payload, created_at)
  VALUES (gen_random_uuid(), 'ord_123', 'order.created', '{"order_id":"ord_123","total_cents":4999}', now());
COMMIT;
</code></pre>
<p>Now atomicity is trivial, because both writes are ordinary rows in the database you already have transactional guarantees for. Either both rows exist or neither does. There is no window where the order exists but its event doesn't, because "the event exists" now just means "a row in the same table transaction says so."</p>
<p>A separate relay process, running continuously, polls the outbox table for unpublished rows (or reads Postgres's write-ahead log directly via logical replication, which is how tools like Debezium do it without polling), publishes each one to the real message broker, and marks it published. If the relay crashes, it resumes from the last unpublished row on restart. If the broker is briefly unavailable, the events just sit in the table until it recovers. The order write itself never blocks on the broker being up at all.</p>
<p>The trade-off is honest, not hidden: downstream consumers now see events after a short delay (however often the relay polls, typically sub-second to a few seconds), and they can receive the same event more than once if the relay publishes successfully but crashes before marking the row as sent. That second part is why every consumer of these events has to be idempotent, not because it's good hygiene in the abstract, but because at-least-once delivery is the actual guarantee the outbox gives you, and "exactly once" isn't achievable without it.</p>
<hr>
<h3>Sagas: The Same Problem, Stretched Across Multiple Services</h3>
<p>The outbox pattern solves atomicity for one write plus one event. A saga is what you reach for when the operation itself spans multiple services, each with its own local database, and there's no way to wrap the whole thing in a transaction even in principle: reserve inventory, charge the payment, create the shipment. Three services, three databases, one logical operation.</p>
<p>A saga runs this as a sequence of local transactions, each one committing on its own, with a <strong>compensating action</strong> defined for each step in case a later step fails:</p>
<ol>
<li>Inventory service reserves the item. Commits locally.</li>
<li>Payment service charges the card. Commits locally.</li>
<li>Shipping service creates the shipment. If this fails...</li>
<li>...run the compensations in reverse: refund the payment, release the inventory reservation.</li>
</ol>
<p>Nothing here is atomic in the ACID sense. There's a real window, between steps 1 and 3, where inventory is reserved and no shipment exists yet. A saga doesn't hide that window, it makes it explicit and time-bounded, and gives you a defined path back to a consistent state if the last step doesn't complete. That's a fundamentally different consistency model than a database transaction (eventual, with defined compensations, instead of immediate and atomic), and pretending otherwise is where sagas go wrong in practice: teams build the happy path, skip writing the compensating actions because "that won't really happen," and then an incident forces someone to write the refund logic live, under pressure, for a case they never tested.</p>
<p>Each step publishing its "I'm done" or "I failed" signal is itself a dual-write problem at a smaller scale, which is why saga implementations lean on the outbox pattern internally for each step, rather than being a separate mechanism from it. The two patterns compose: outbox solves atomic local write-plus-event, sagas solve the multi-step orchestration built on top of that primitive.</p>
<hr>
<h3>What This Actually Demonstrates</h3>
<p>None of this is about picking the "advanced" pattern to look sophisticated. It's about recognizing that "write to the database, then tell everyone else" is not one operation, it's two, and the honest response to that is either to make the second write ride inside the first one's transaction (Outbox) or to make the multi-step version of that gap explicit and recoverable (Sagas) — not to assume the gap won't matter until an incident proves otherwise.</p>]]></content:encoded>
      <pubDate>Sat, 29 Aug 2026 03:28:24 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>System Design</category>
      <category>Distributed Systems</category>
      <category>Backend</category>
      <category>Architecture</category>
    </item>
    <item>
      <title>Indexing a Blockchain in Real Time, at Scale</title>
      <link>https://insight.jatinjainsaraf.com/case-study/indexing-a-blockchain-in-real-time-at-scale</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/case-study/indexing-a-blockchain-in-real-time-at-scale</guid>
      <description>A blockchain never pauses for maintenance. Here&apos;s how SupraScan&apos;s indexer stays caught up, decomposes every transaction into a dozen relational tables, and survives its own concurrency bugs without losing data.</description>
      <content:encoded><![CDATA[<p>Most backend systems get a quiet window somewhere. A maintenance mode, a deploy freeze, a Sunday night when traffic drops and you can safely change something load-bearing. A blockchain indexer doesn't get that window. The chain produces a new block whether your system is ready for it or not, and every second you're not caught up is a second of history you have to make up later, under load, while more blocks keep arriving.</p>
<p>That's the actual engineering problem behind SupraScan, the block explorer and indexing layer for the Supra network. Its job sounds simple: consume every block, parse every transaction, write structured data to PostgreSQL, with no missed blocks, no duplicate processing, and no gaps in the indexed history. The interesting part isn't any single piece of that sentence. It's that all of it has to happen continuously, at production throughput, forever, with the failure modes only showing up once you're running at real scale.</p>
<h2>What "no pause button" actually costs you</h2>
<p>Three constraints shape everything else about this system.</p>
<p><strong>Throughput.</strong> Production now runs at 500–1,000 TPS. An earlier benchmark run measured 110-118 TPS sustained across 10 running instances, enough on its own to make naive designs fall over, and at that load Postgres itself sat at 50% CPU idle. The database wasn't the bottleneck. The RPC layer was, throwing socket-hangup errors and climbing past 600ms latency under the same load. That single number reframes the whole architecture problem: you're not primarily fighting the database, you're fighting how fast you can safely pull data out of the chain node and fan it out to workers.</p>
<p><strong>Ordering, without the safety net most indexers get.</strong> Blocks have to land in strict sequence, block N fully processed before block N+1 is trusted. Most chain indexers also have to handle reorgs, the chain deciding retroactively that a block it already gave you doesn't count anymore, and rolling back whatever you'd already written. Supra has instant finality, so that entire class of complexity doesn't exist here. It's a real simplification, and it's worth naming, because it's easy to assume every blockchain indexer needs rollback logic. This one doesn't.</p>
<p><strong>No central authority.</strong> Every worker instance is equal. Any worker can crash at any moment, mid-block, mid-write, without warning. Nothing about the design can assume a single instance stays alive to coordinate the rest. That constraint is what makes the rest of this interesting.</p>
<h2>The shape of the system</h2>
<p>Redis is the coordination layer: lock state, progress tracking, health signals. Postgres is the store of record. The blockchain's RPC endpoint is the source of truth for block data itself.</p>
<p>On startup, each pod goes through a fixed sequence: connect to the database, start the partition-management service (creates the next several days of table partitions automatically, ahead of when the data lands), run any pending one-time jobs, and start polling for the current chain height. Several of these services, and a few others, only run on one pod at a time. Leader election over a Redis heartbeat decides who does TPS calculation, fee calculation, and periodic stats. Every pod, leader or not, does the actual work: pulling blocks, indexing transactions, indexing wallets. The leader role exists to stop duplicate work on shared aggregates, not to gate the core pipeline behind a single instance.</p>
<p>Once running, the main loop is intentionally simple: check whether processing is paused, claim the next batch of block numbers, process them, log throughput, and loop again with almost no delay between iterations. The interesting decisions all live one level down, in how "process them" adapts to how far behind the system currently is.</p>
<h2>A system that changes its own strategy under load</h2>
<p>The indexer runs in one of two modes, and it switches automatically based on measured lag. When it's within a small number of blocks of the chain tip, it processes one block at a time: every block in the current range gets handled concurrently, one RPC call per block, full write pipeline per block, no batching overhead. Once it falls further behind than that, it switches into a batch mode, pulling a larger group of blocks at once and firing all their RPC calls concurrently, trading a bit of per-block latency for a lot more throughput. If a batch RPC call fails, it doesn't just retry the batch. After a couple of failed attempts it falls back to processing that range one block at a time, trading speed for reliability once concurrency itself looks like the problem.</p>
<p>This is a small design decision that pays for itself constantly: a system that's usually keeping up gets the latency profile of single-block processing, and a system that's falling behind (a slow node, an RPC hiccup, a burst of transaction volume) automatically shifts into a mode built for catching up, without anyone paging on-call to flip a setting.</p>
<h2>One transaction, a dozen tables</h2>
<p>Here's where the fan-out really shows up. A single indexed transaction isn't one row. Inside the write path, one transaction batch triggers somewhere between 15 and 20 sequential database operations: the core transaction record, an "advanced information" record (payload detail), deletion of any stale associated data from a prior attempt, event records, sender/receiver/fee-payer records, then separate tables for coin transfers, fungible asset transfers, NFT transfers, and automation registration, cancellation, execution, and gas-assessment records where relevant. It's a full relational decomposition of one on-chain event, not a JSON blob dropped into a single column.</p>
<p>That decomposition is what makes the system queryable at all, but it's also where a genuinely subtle constraint shows up. Senders, receivers, and fee-payer tables carry database triggers that decrement a wallet's transaction count on delete, and the code that manages this is explicit about why it can't be parallelized: bulk-deleting across multiple transactions concurrently causes concurrent trigger executions on the same wallet row, which either deadlocks or silently miscounts. A comment in the transaction repository says it plainly: deletions happen one at a time specifically to avoid that. It's not an oversight or unfinished optimization. It's a constraint the team hit, understood, and left documented in the code, because the "obvious" faster version is the one that corrupts data under load.</p>
<h2>Failures get quarantined, not swallowed</h2>
<p>Any block that fails processing three times doesn't get retried forever inline. It drops into a dead-letter topic, out of the main pipeline's way, and a separate reprocessing service picks it back up in chunks. That service is deliberately run on a single instance in production, not because it wouldn't be nice to parallelize, but because its own claim function has no row-locking mechanism to prevent two pods from grabbing the same message. Rather than build that locking immediately, the honest tradeoff was made to run it single-instance and accept the smaller throughput ceiling on the recovery path, which handles a small fraction of total volume, in exchange for correctness without added complexity.</p>
<p>This is the same instinct that shows up in the leader-election pattern for aggregate services: constrain concurrency exactly where correctness demands it, and leave everything else free to scale horizontally.</p>
<h2>A concurrency bug the design didn't originally account for, and a different one still open</h2>
<p>The main loop used to claim its next batch of blocks by writing a control key to Redis, then reading it back on the next iteration to know where to pick up. That write-then-read pattern looks harmless in isolation. Under real multi-pod load during a catch-up run, it wasn't. A measured benchmark across 10 pods under lag showed a 4.01x duplication ratio: pods were collectively doing four times the useful work in raw completions, because the shared coordination key was being read after a multi-second processing window had already passed, not claimed atomically at the start of it.</p>
<p>It's worth being precise about what this bug did and didn't do. It didn't corrupt data. Every write in the pipeline is idempotent by design, so overlapping work from two pods processing the same range produced the same end state, not conflicting ones. What it cost was infrastructure efficiency: real compute and real RPC calls spent on collisions instead of throughput, at exactly the moments, catch-up under lag, when throughput matters most. It's also specifically a multi-pod problem: a live check of production mainnet's actual deployment configuration confirmed it currently runs as a single instance, which means single-instance coordination didn't strictly need this fix to be correct in that specific environment. The fix shipped anyway, because any environment or future scale-out that does run multiple instances would otherwise inherit the race immediately: the write-then-read claim was replaced with a single atomic <code>INCRBY</code>-based claim, so two pods reading and writing at once now get distinct, non-overlapping ranges by construction instead of racing through a window between two separate calls.</p>
<p>A different, smaller-scope version of the same class of bug is still open, not fixed. The leader-election check that decides which single pod runs the aggregate services, TPS calculation, fee calculation, periodic stats, uses a check-then-set pattern rather than a single atomic operation, and that carries its own theoretical race during the moment two pods could both see no current leader and both try to claim the role. A proposed fix exists, replacing it with a single atomic conditional set, but it hasn't shipped. It's a smaller blast radius than the block-range race was, duplicate leadership would mean redundant aggregate computation, not duplicate block processing, but it's the same category of gap: understood, scoped, and honestly still sitting there unfixed.</p>
<p>It's also worth naming an alternative that was tried at the transport layer and abandoned, not just tuned. Block delivery in this system doesn't run through a message-streaming platform; an earlier version of the architecture did use one, and it was removed in favor of a simpler, database-backed work queue paired with the same Redis coordination layer described above. That's a real, lived example of the opposite failure mode from the one usually worried about in architecture discussions, not under-engineering, but a genuine willingness to walk back a heavier piece of infrastructure once a simpler one covered the same need with less to operate.</p>
<h2>What this actually enables</h2>
<p>The dead-letter and reprocess design isn't just a safety net for today's failure modes. It's the same mechanism that absorbed a real production incident when the chain started emitting a new transaction type the indexer's release branch hadn't been updated to handle safely, crashing on unguarded property access exactly on that reprocessing path. Because the recovery path already existed, the fix was a scoped hotfix, not an emergency rebuild.</p>
<p>Having closed the multi-pod race mattered for a specific reason beyond the efficiency it recovered at the time. Supra's MultiVM work means blocks will start carrying transactions from multiple virtual machines simultaneously, which increases both volume and per-transaction complexity. A coordination layer that quietly wasted 2-4x its capacity to collisions would have been a much worse foundation for that next phase than one that doesn't. Fixing it ahead of that was directly in service of scaling for what's coming, not abstract cleanup, and the still-open leader-election race is worth closing for the same forward-looking reason, before a busier, multi-VM future makes any coordination gap more expensive to leave sitting.</p>
<h2>What this demonstrates</h2>
<p>None of the individual pieces here are exotic. Leader election, adaptive batching, dead-letter queues, and idempotent writes are all known patterns. What's harder to fake is the discipline underneath them: a system that adjusts its own throughput strategy based on measured lag instead of a fixed setting, a data model that decomposes correctly under a documented, hard-won concurrency constraint, and a team that can point to a real, unfixed inefficiency in its own coordination layer and explain exactly why it hasn't caused a correctness problem yet. Production systems that run continuously, with no maintenance window and no single point of coordination, get built by making these tradeoffs explicitly and writing down the ones you haven't closed yet, not by pretending you've already closed all of them.</p>]]></content:encoded>
      <pubDate>Sat, 22 Aug 2026 15:49:44 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Case Study</category>
      <category>case-study</category>
      <category>architecture</category>
      <category>blockchain</category>
      <category>distributed-systems</category>
      <category>postgresql</category>
      <category>redis</category>
    </item>
    <item>
      <title>Elasticsearch Isn&apos;t Dead. You Probably Don&apos;t Need It</title>
      <link>https://insight.jatinjainsaraf.com/elasticsearch-isnt-dead-you-probably-dont-need-it</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/elasticsearch-isnt-dead-you-probably-dont-need-it</guid>
      <description>Search gets slow, someone says &apos;we need Elasticsearch,&apos; and two weeks later there&apos;s a new cluster, a sync pipeline, and a class of bugs that didn&apos;t exist before. PostgreSQL&apos;s tsvector, GIN indexes, and pg_trgm cover the full-text and fuzzy-matching workload most teams actually have, without a second database to keep in sync. This is the case for starting there and adding Elasticsearch only when the workload actually demands it.</description>
      <content:encoded><![CDATA[<h1>Elasticsearch Isn't Dead. You Probably Don't Need It.</h1>
<p>Every team hits the same moment. Search gets slow, someone says "we need Elasticsearch," and two weeks later there's a new cluster, a sync pipeline, and a Slack channel called <code>#search-is-down</code>.</p>
<p>Here's the case for not doing that. PostgreSQL's built-in full-text search handles the kind of workload many teams actually have, and it does it without adding a second database to the architecture.</p>
<h2>The real cost isn't the cluster, it's the copy</h2>
<p>Adding Elasticsearch doesn't add a search feature. It adds a second brain that has to keep agreeing with your first one.</p>
<p>Your data lives in Postgres. Now it also has to live in Elasticsearch, duplicated, reshaped into documents, kept current through a queue or a change-data-capture pipeline or a reindex job someone wrote in a hurry two years ago. Every insert, update, and delete in Postgres needs a matching write on the other side. That sync layer is where the real cost lives, not in cluster fees:</p>
<ul>
<li>A customer updates their email. Postgres has the new one. Elasticsearch still returns the old one for six hours because the CDC consumer fell behind.</li>
<li>A row gets deleted in Postgres. The reindex job crashed last Tuesday, so it still shows up in search, and support gets a ticket about a ghost customer.</li>
<li>Someone runs a backfill migration, forgets the matching backfill in Elasticsearch, and search results quietly diverge from the database for a month before anyone notices.</li>
</ul>
<p>None of these are Elasticsearch bugs. They're the tax you pay for keeping two representations of the same data that don't share a transaction boundary. Postgres full-text search skips this entirely, because there's nothing to sync. The searchable representation lives in the same database and is maintained transactionally with the data.</p>
<h2>What Postgres actually gives you: tsvector and tsquery</h2>
<p>Full-text search in Postgres is built on two pieces: <code>tsvector</code>, which turns text into a normalized, searchable format, and <code>tsquery</code>, which turns a search string into something you can match against it.</p>
<pre><code class="language-sql">SELECT to_tsvector('english', 'PostgreSQL indexes make searches fast');
-- 'fast':5 'index':2 'make':3 'postgresql':1 'search':4
</code></pre>
<p><code>to_tsvector</code> normalizes the text, removes stopwords according to the text-search configuration (here "make" survives, it isn't one), and applies stemming where the configured dictionary supports it. That's why a search for <code>search</code> can match text containing <code>searching</code>, the same behavior that makes Elasticsearch feel necessary in the first place.</p>
<pre><code class="language-sql">SELECT to_tsvector('english', 'PostgreSQL indexes make searches fast')
       @@ to_tsquery('english', 'search &#x26; index');
-- true
</code></pre>
<p>The <code>@@</code> operator matches a <code>tsvector</code> against a <code>tsquery</code>. That's full-text matching as a native Postgres operator, not a separate search service. It doesn't score relevance on its own, that's what <code>ts_rank</code> is for below.</p>
<p>For a real search box, <code>to_tsquery</code> is less useful than it looks, because it throws a syntax error on ordinary user input like an unbalanced quote or a trailing <code>&#x26;</code>. <code>websearch_to_tsquery</code> is the better default: it accepts web-search-style syntax (quoted phrases, <code>-exclude</code>, <code>or</code>) and never rejects plain text.</p>
<pre><code class="language-sql">SELECT title, ts_rank(search_vector, query) AS rank
FROM articles, websearch_to_tsquery('english', $1) query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;
</code></pre>
<p>Reach for <code>to_tsquery</code> when you control the query syntax yourself. Reach for <code>websearch_to_tsquery</code> when the query comes from a search box.</p>
<h2>The part that makes it production-ready: GIN indexes</h2>
<p>If you compute the <code>tsvector</code> during every query, Postgres has to process the candidate rows to build those vectors on the fly, fine for a thousand rows, a real cost at ten million. For data that's searched regularly, storing the vector and indexing it with a GIN index (Generalized Inverted Index) avoids that repeated work.</p>
<pre><code class="language-sql">ALTER TABLE articles ADD COLUMN search_vector tsvector
  GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;

CREATE INDEX articles_search_idx ON articles USING GIN (search_vector);
</code></pre>
<p>That <code>GENERATED ALWAYS AS ... STORED</code> column is the detail that matters in production. Postgres computes it whenever the underlying row is inserted or updated, and the GIN index is maintained as part of the same database transaction, the same way any index is maintained when its underlying column changes. There's no separate consumer to fall behind and no second system that can temporarily disagree with the row.</p>
<p>A GIN index works like the index at the back of a textbook. Instead of scanning every page for the word "vacuum," you jump straight to the pages listed under V. Postgres does the same: instead of scanning every row's <code>tsvector</code>, it jumps straight to the rows containing your search terms.</p>
<p>Worth saying plainly: that index isn't free. It costs write overhead on every insert and update, and it costs disk, same as any index does. The difference isn't zero cost versus some cost, it's an index cost you were always going to pay somewhere versus that same index cost plus a whole second system to keep in sync. One cost, not two.</p>
<p><code>ts_rank</code> scores matches by relevance, giving you the ranking step you'd otherwise reach for a search engine to provide. The query runs inside Postgres, alongside the rest of your relational data, without a network hop to a separate search cluster.</p>
<h2>Filtering is where a bolted-on search engine shows its seams</h2>
<p>Most real search features aren't just "find text," they're "find text, then filter by category, status, or whatever's live right now." If Elasticsearch is populated asynchronously from Postgres, that filter is only as current as the index. A row can go inactive in Postgres while the search index still considers it active, for however long the sync pipeline lags. In Postgres, the filter and the full-text predicate run against the same transactional dataset in the same <code>WHERE</code> clause. There's no separate search index that can lag behind the row, because the filter and the full-text predicate operate on the same Postgres data.</p>
<p>That single detail, an asynchronously maintained index lagging the source of truth, is usually the thing that quietly breaks in production long after the Elasticsearch launch party is over.</p>
<h2>Where Elasticsearch still earns its keep</h2>
<p>This isn't "Elasticsearch is bad." It's "most teams adopted it for a problem they didn't have."</p>
<p>Elasticsearch is worth reaching for when you need fuzzy matching and aggressive autocomplete beyond what <code>pg_trgm</code> comfortably covers, sophisticated relevance tuning, large-scale aggregations and faceting, distributed search across genuinely large datasets, log and observability workloads, or a search workload you deliberately want isolated from your transactional database. Any one of those is a real reason. None of them are "search felt slow once."</p>
<p>And Postgres full-text search isn't a drop-in replacement for all of that. If your product depends on typo tolerance, heavy autocomplete, deep linguistic analysis, or search-specific aggregations at real scale, the tradeoff changes quickly, and that's exactly when Elasticsearch's cost starts paying for itself.</p>
<p>Postgres does cover more of that ground than people expect, though, particularly the typo tolerance part. <code>pg_trgm</code> handles a different class of problem from full-text search: finding strings that are similar even when the spelling isn't exact, which is what powers a lot of "did you mean" and fuzzy-match behavior.</p>
<pre><code class="language-sql">CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE INDEX articles_title_trgm_idx
  ON articles USING GIN (title gin_trgm_ops);

SELECT title
FROM articles
WHERE title % 'elastcsearch'
ORDER BY similarity(title, 'elastcsearch') DESC
LIMIT 10;
</code></pre>
<p>Full-text search matches words and lexemes. <code>pg_trgm</code> matches approximate string similarity. Elasticsearch earns its place when you need substantially more search infrastructure than either of those provides.</p>
<h2>The actual decision</h2>
<p>This was never really "Postgres vs. Elasticsearch." It's "don't add a second system until you've established that Postgres isn't enough."</p>
<p>Start with Postgres. Add full-text search. Add a GIN index. Add <code>pg_trgm</code> if you need fuzzy matching. Measure. Introduce Elasticsearch when the workload actually demands it, not when search first feels slow.</p>
<p>For many teams, that measurement never gets there. The search bar was never the hard part. Keeping two systems telling the same story was.</p>]]></content:encoded>
      <pubDate>Fri, 21 Aug 2026 01:34:27 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Elasticsearch</category>
      <category>Full-Text Search</category>
      <category>Database</category>
      <category>Backend</category>
    </item>
    <item>
      <title>Why Your Docker Image Is 3GB and Nobody Noticed Until the Deploy Timed Out</title>
      <link>https://insight.jatinjainsaraf.com/docker-image-bloat-stale-layers</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/docker-image-bloat-stale-layers</guid>
      <description>A deploy that used to take ninety seconds now takes six minutes, and nobody changed the code. The real story is in how Docker layers, build caching, and registries accumulate weight that never comes back off on its own, plus how to actually measure it before you guess.</description>
      <content:encoded><![CDATA[<h1>Why Your Docker Image Is 3GB and Nobody Noticed Until the Deploy Timed Out</h1>
<p>A service that used to deploy in ninety seconds now takes six minutes. Nobody changed the code that day. The base image hasn't changed, <code>docker push</code> is uploading to the same registry it always has, and the CI runner is the same size it always has been. What changed is that the image itself quietly grew, build after build, revision after revision, until one day the push step timed out and someone finally looked. Nothing broke on purpose. The image just never stopped getting heavier, and nobody was watching the number.</p>
<hr>
<h3>An Image Is a Stack of Layers, Not a Snapshot</h3>
<p>A Docker image isn't one file, it's a stack of read-only layers, one per instruction in the Dockerfile that changes the filesystem: a <code>RUN</code>, a <code>COPY</code>, an <code>ADD</code>. Each layer is a diff against the one below it, and at runtime the container's filesystem is presented as the combination of all those layers through a union filesystem, with a thin writable layer on top for the container itself. The layers stay separate on disk; nothing flattens them into one file. Crucially, a layer never shrinks once written. If a <code>RUN apt-get install</code> layer downloads 400MB of packages and a later <code>RUN apt-get clean</code> layer deletes the package cache, the image doesn't get 400MB smaller. The delete happens in a new layer on top; the old layer with the 400MB still sits underneath it, and that layer remains part of the image that nodes need to have available. The deleted file disappears from the filesystem you actually see when the container runs, but the bytes are still physically present in the image and still contribute to its size. If a registry or node doesn't already have that layer, those bytes have to be transferred during a push or pull.</p>
<p>Think of it like a suitcase you never fully unpack between trips. You don't take everything out and repack from scratch, you just add what you need for this trip on top of what's already in there. Take something out and it's not gone, it's still in the suitcase, just buried under a note that says "ignore this." The suitcase only gets heavier. That's a Docker image with unpruned layers: every stale cache, every intermediate build tool, every "temporary" file that got <code>rm</code>'d in a later step is still physically present, just marked as superseded.</p>
<p><strong>Why this shows up in production:</strong> a base image with a full OS toolchain (Ubuntu with build-essential, a full Node.js image instead of a smaller variant) starts you 500MB to 1GB heavier before your application code exists at all. Every dependency installed and not cleaned up in the same layer, every <code>COPY . .</code> that grabs <code>node_modules</code>, <code>.git</code>, and test fixtures because there's no <code>.dockerignore</code>, adds weight that never comes back off. None of it fails a build. It just makes every image bigger than the last one, silently, until push and pull times are the bottleneck. A rough breakdown of how an image gets to 3GB:</p>
<pre><code class="language-text">Base image                900 MB
Build dependencies        700 MB   (compilers, headers, dev packages)
node_modules               500 MB
Source and test files      200 MB
Deleted-but-retained data  700 MB  (old caches, files removed in a later layer)
------------------------------------
Total                     ~3.0 GB
</code></pre>
<p>No single layer looks unreasonable on its own. It's the accumulation across all of them, plus the ones that should have been discarded but structurally can't be, that adds up.</p>
<h3>Why Bad Layer Ordering Makes Every Build Slower</h3>
<p>Layer caching is supposed to make builds fast: Docker walks the Dockerfile instruction by instruction and reuses the cached result for each one, until it hits the first instruction whose cache is invalid, either because the instruction itself changed or because a file it depends on changed. From that instruction onward, every remaining layer has to be rebuilt, even if most of them don't actually depend on what changed. That's the entire point of the layer model, and it only works if the Dockerfile is ordered so that the things which change least often come first, and the things that change on every commit come last.</p>
<p>The single most common mistake is <code>COPY . .</code> before the dependency-install step:</p>
<pre><code class="language-dockerfile"># Bad: any file change invalidates the install layer
COPY . .
RUN npm install
</code></pre>
<p>Every commit touches some file in the repo, so that <code>COPY</code> layer's cache is invalidated on every build, and <code>npm install</code> right after it has to rerun from a cold cache every time, even when <code>package.json</code> didn't change. Reordering it fixes the problem directly:</p>
<pre><code class="language-dockerfile"># Better: only a change to package*.json invalidates the install layer
COPY package*.json ./
RUN npm ci
COPY . .
</code></pre>
<p>Now changing <code>src/foo.js</code> invalidates only the final <code>COPY</code>, and the dependency-install layer above it stays cached. A build that might otherwise take fifteen seconds because nothing dependency-related moved instead can take three minutes on the bad version, on every revision, forever, because the Dockerfile put the wrong instruction first.</p>
<p><strong>Why this shows up in production:</strong> this is the mechanism behind "deploys used to be fast and now they're not," with no code change to point at. It's not the application getting slower, it's the build losing its cache on every run because of layer ordering, on top of an image that's also grown heavier over time, so both the build step and the push/pull step degrade independently and get blamed on each other.</p>
<h3>The Registry Doesn't Forget Either</h3>
<p>The same "nothing shrinks unless someone tells it to" problem exists one level up, in the artifact registry itself. Every build that pushes a new tag, <code>latest</code>, a commit SHA, a version number, adds a new manifest and a new set of layer blobs to the registry. Layers that are byte-identical get deduplicated by content hash, which is why the registry doesn't grow linearly with every push, but every layer that's actually different (a new dependency version, a new base image patch, a rebuilt application layer) is new data that has to be stored, and old images don't get deleted just because a newer one exists.</p>
<p>A registry doesn't necessarily know which images are no longer useful. Unless you give it retention rules or clean them up yourself, old manifests and their unique layer blobs just accumulate: every feature-branch build, every hotfix, every image built by a CI run that never got cleaned up, all still sitting there, still counted against storage. A registry with a two-year-old image nobody has pulled in eighteen months isn't just wasted storage, it's an image nobody is auditing for CVEs anymore, sitting right next to the one that's actually in production, indistinguishable to anyone browsing tags without a naming convention.</p>
<p><strong>Why this shows up in production:</strong> registry storage bills climbing with no obvious cause, and CI jobs or registry maintenance operations getting slower or more expensive as a repository accumulates large numbers of manifests and unique layer blobs. A single <code>docker pull</code> for a specific tag isn't slowed down by unrelated stale tags sitting elsewhere in the repository, it only fetches the layers that tag's manifest actually references, but repository-wide operations, listing tags, running garbage collection, scanning for vulnerabilities across everything stored, all get heavier as the pile grows. The fix isn't a bigger registry plan, it's a retention policy: expire untagged manifests and feature-branch tags after N days, keep only the last M builds per branch, and let content-addressable deduplication do the rest.</p>
<h3>Measure Before You Guess</h3>
<p>Before reordering a Dockerfile or swapping a base image, find out what's actually taking up space. Two commands answer that directly:</p>
<pre><code class="language-bash">docker image history myapp:latest
</code></pre>
<p>Lists every layer in the image with its size, in order, so you can see exactly which instruction added the most weight.</p>
<pre><code class="language-bash">docker image inspect myapp:latest
</code></pre>
<p>Gives the full layer manifest and metadata, useful for scripting size checks in CI. For a closer look at what's actually sitting inside the largest layers, a tool like <a href="https://github.com/wagoodman/dive"><code>dive</code></a> walks the filesystem contents layer by layer, which is usually faster than guessing from the Dockerfile alone. Run <code>docker image history</code> first: it takes thirty seconds and tells you whether the problem is the base image, an unpruned dependency cache, or the build context, before you change anything.</p>
<h3>What Actually Fixes It</h3>
<ul>
<li><strong>Order the Dockerfile by change frequency.</strong> Dependency manifests (<code>package.json</code>, <code>requirements.txt</code>) get copied and installed first, source code gets copied last. That one reordering is usually the single biggest build-time win available.</li>
<li><strong>Use multi-stage builds.</strong> Build in one stage with the full toolchain, compilers, dev dependencies, and copy only the compiled output into a clean final stage. The build tools never make it into the image that ships. A simplified Node example, for a service that produces a self-contained <code>dist</code> directory:</li>
</ul>
<pre><code class="language-dockerfile">FROM node:22 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/index.js"]
</code></pre>
<p>The final image only contains what <code>COPY --from=build</code> explicitly pulls forward: the compiled output and production dependencies. The compiler, dev dependencies, and source files from the build stage never exist in the shipped image at all.</p>
<ul>
<li><strong>Pick a smaller runtime image where it makes sense.</strong> <code>-slim</code>, distroless, or Alpine variants are often the difference between a 900MB image and a 90MB one before any application code is added, but they're not a free win in every case: Alpine's musl libc can surface compatibility issues with some native dependencies, so weigh the size/security gain against your actual runtime's tolerance for it rather than defaulting to it blindly.</li>
<li><strong>Write a real <code>.dockerignore</code>.</strong> <code>node_modules</code>, <code>.git</code>, test fixtures, and local env files should never be in the build context in the first place. This keeps the image smaller directly, and it also keeps irrelevant files from ever reaching a <code>COPY</code> layer and invalidating its cache for no reason.</li>
<li><strong>Set a registry retention policy.</strong> Expire untagged and stale feature-branch images automatically instead of relying on someone remembering to clean up. Most registries (ECR, Artifact Registry, GitHub Container Registry, Harbor) support this natively.</li>
</ul>
<p>None of these are exotic. They're three separate mechanisms with the same operational lesson: unused data doesn't disappear unless you design for it or clean it up on purpose. A service that deploys in six minutes instead of ninety seconds usually isn't a mystery. It's a suitcase that's been repacked on top of itself for a year, and nobody's taken anything out.</p>]]></content:encoded>
      <pubDate>Wed, 19 Aug 2026 17:41:42 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Docker</category>
      <category>DevOps</category>
      <category>Performance</category>
    </item>
    <item>
      <title>Why Postgres and Cassandra Made Opposite Bets on Storage Engines</title>
      <link>https://insight.jatinjainsaraf.com/why-postgres-and-cassandra-made-opposite-bets-on-storage-engines</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/why-postgres-and-cassandra-made-opposite-bets-on-storage-engines</guid>
      <description>A write-heavy ingestion table on Postgres starts choking under load: autovacuum can&apos;t keep up, WAL grows fast, and every insert costs more than it should. A Cassandra table doing the same job barely notices. The difference isn&apos;t tuning. It&apos;s a decision made before either database wrote a single line of code: B-Tree or LSM-Tree.</description>
      <content:encoded><![CDATA[<h1>Why Postgres and Cassandra Made Opposite Bets on Storage Engines</h1>
<p>A write-heavy ingestion table starts slow on Postgres. Not immediately, but a few weeks in: inserts that used to take a millisecond now take ten, autovacuum is perpetually behind, and <code>EXPLAIN ANALYZE</code> shows time going into index maintenance nobody remembers configuring. The instinct is to blame the schema, or the hardware, or "Postgres doesn't scale." None of those are quite right. The real answer is a decision Postgres made in the 1990s, long before this table existed: it keeps its indexes as B-Trees, and every one of them has to stay sorted, on every write.</p>
<hr>
<h3>Every Write Has to Land Somewhere</h3>
<p>Strip a database down to its storage engine and the job is always the same: take a write, put it on disk in a shape that makes future reads fast, and don't lose it. There are two dominant answers to how to do that, and most databases you've heard of lean on one.</p>
<p><strong>B-Trees</strong> keep some part of the data sorted on disk, in place, at all times. MySQL's InnoDB and SQLite go all the way: the table itself is a B-Tree, keyed by its primary key or rowid, so the table and its main index are the same structure (a "clustered index"). Postgres is more layered: the table (the heap) is an unordered file with no sort order of its own, rows just go wherever the free space map says there's room, but every index on that table, including the primary key, is a separate B-Tree that has to stay sorted and has to be updated on every write that touches an indexed column.</p>
<p><strong>LSM-Trees</strong>, log-structured merge trees (Cassandra, RocksDB, LevelDB, and CockroachDB's Pebble storage engine), take the opposite approach everywhere: a write never touches its final sorted position immediately. It gets appended to whatever's currently open, and getting everything back into sorted order is a job for later, done in the background, in bulk.</p>
<p>That fork, sorted-in-place versus sorted-later, explains almost every practical difference in how these two families of databases behave under load.</p>
<h3>B-Trees: Pay at Write Time, Save at Read Time</h3>
<p>Think of each B-Tree index as a filing cabinet that's always perfectly alphabetized. Every entry goes directly into its correct folder, in its correct position, the moment it arrives. Finding anything later is fast and predictable, you walk straight to the folder. But filing it correctly in the first place means locating the right spot, possibly shifting other entries out of the way, and writing to a specific place on disk rather than just the next free spot.</p>
<p>Here's what a Postgres insert actually does. The row itself is appended to the heap, close to a sequential write, wherever the free space map finds room. But every index on that table, the primary key, any unique constraint, any column you've indexed for lookups, is a B-Tree, and each one needs a new entry written to its correct leaf page, wherever that page happens to sit on disk. Two indexes on a table means one heap append plus two separate random writes, every single insert.</p>
<p>Updates are the more expensive case, and this is where MVCC changes the arithmetic. Postgres never overwrites a row in place: an <code>UPDATE</code> marks the old row version dead and writes an entirely new version elsewhere in the heap. That means an <code>UPDATE</code> costs everything an <code>INSERT</code> costs, a new heap entry, plus a new entry in every index, plus it leaves a dead tuple behind that autovacuum eventually has to clean up.</p>
<p><strong>Why this shows up in production:</strong> early in a table's life, its hot pages and index pages fit comfortably in <code>shared_buffers</code>, so those "random" writes are really just writes to RAM, flushed to disk lazily and cheaply. Once the table and its indexes outgrow memory, every insert or update has a real chance of touching an index page that isn't cached, turning a cheap write into a disk seek, on every indexed column, on every write. Add autovacuum needing to revisit the dead tuples MVCC leaves scattered across heap pages, and you get the exact symptom this post opened with: a table that used to be fast getting steadily slower with no code change, just growth, and just more indexes to maintain per write.</p>
<h3>LSM-Trees: Write First, Sort Later</h3>
<p>An LSM-Tree handles the same problem by refusing to do that work up front. Picture an inbox tray instead of a filing cabinet: every new entry just gets dropped on top of the pile. Filing is instant, because there's no filing, you're not finding anything, you're just adding to a stack. Periodically, someone takes the whole pile, sorts it properly, and merges it into the already-sorted sections below it.</p>
<p>Mechanically: writes go into an in-memory structure (a memtable) and are also logged to a commit log for durability, both sequential operations. When the memtable fills up, it's flushed to disk as an immutable sorted file (an SSTable). Over time you accumulate many of these sorted files, and a background process called <strong>compaction</strong> merges them together, discarding values that have been overwritten or deleted along the way.</p>
<p>Deletes are handled the same indirect way updates are avoided: a delete doesn't remove anything immediately, it writes a <strong>tombstone</strong>, a marker saying "this key is deleted as of this timestamp." The actual old data only disappears once compaction physically rewrites the SSTables that contained it.</p>
<p><strong>Why this shows up in production:</strong> writes are cheap and consistently fast, because they're always sequential appends, regardless of how big the dataset gets or where the "right place" for the data would eventually be. That's why Cassandra-style databases are the default reach for write-heavy workloads: time-series ingestion, event logs, metrics pipelines. The cost is deferred, not eliminated. A read might have to check the memtable and several SSTables before it can be sure it has the latest version of a key, that's <strong>read amplification</strong>, and it's the mirror image of the index-maintenance cost a B-Tree pays on every write. Compaction itself is a real, ongoing background cost too: it consumes disk I/O and CPU that has to be budgeted for, and if compaction ever falls behind sustained write load, both read latency and disk usage climb, in a way that looks a lot like Postgres autovacuum falling behind, just triggered by the opposite kind of pressure.</p>
<h3>"Just Switch to Cassandra" Isn't the Fix It Sounds Like</h3>
<p>It's tempting to read the above and conclude LSM-Trees are strictly better for anything write-heavy. They're not, they're differently shaped, with their own failure modes:</p>
<ul>
<li><strong>Read amplification is real.</strong> A point lookup that's a single B-Tree traversal in Postgres might mean checking a memtable plus several SSTables in an LSM-Tree, unless bloom filters and careful compaction keep that bounded.</li>
<li><strong>Compaction is not free.</strong> It's extra write I/O (rewriting data that was already written once), and a compaction strategy mismatched to the workload (size-tiered vs. leveled) can make total write cost in an LSM-Tree worse than a B-Tree's, not better.</li>
<li><strong>Tombstones linger.</strong> Until compaction actually removes the old versions and their tombstones, deleted data still occupies space and still gets scanned past on reads, and in Cassandra specifically, an excess of tombstones scanned in a single read can trip query-side warnings or failures.</li>
<li><strong>Transactional guarantees differ.</strong> Postgres gives you multi-row ACID transactions and foreign keys as first-class features because the B-Tree indexes, MVCC, and WAL were co-designed for exactly that. Most LSM-based systems trade some of that away for horizontal write scale, which is a real cost if your workload actually needs cross-row consistency, not just fast ingestion.</li>
</ul>
<p>Migrating a workload from Postgres to Cassandra to solve a write-scaling problem, without checking whether the workload also needs the guarantees Postgres was giving up in the trade, is how you end up rebuilding transactional consistency by hand in application code, which is a worse position than the one you started in.</p>
<h3>When Each Model Actually Wins</h3>
<p>The honest framework isn't "which database is better," it's "which cost am I willing to pay":</p>
<ul>
<li><strong>Choose a B-Tree-indexed engine</strong> (stay on Postgres, tune it) when reads dominate, or when transactional consistency and relational integrity across tables matter more than raw write throughput. Most application backends, most OLTP workloads, most systems where "this row is correct right now" matters, belong here.</li>
<li><strong>Choose an LSM-Tree engine</strong> when the workload is write-dominated and append-heavy by nature, event streams, time-series metrics, logs, and reads can tolerate either eventual consistency or a slightly higher read cost in exchange for writes that don't degrade as the dataset grows.</li>
<li><strong>Or, more often than either extreme:</strong> the write-heavy table causing the pain doesn't need to be in Postgres at all. Partitioning it, moving it to a purpose-built time-series or log store, or reducing the number of indexes it maintains, frequently solves the actual problem without touching the primary transactional database's engine at all.</li>
</ul>
<p>Postgres isn't losing to Cassandra when a table gets slow under heavy writes. It's honoring the trade it made on day one: keep every index sorted and correct on every write, so reads stay cheap and consistent forever. Whether that's still the right trade for a specific table, and how many indexes it really needs, is a workload question, not a verdict on the database.</p>]]></content:encoded>
      <pubDate>Sat, 15 Aug 2026 09:54:24 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Architecture</category>
      <category>System Design</category>
      <category>Backend</category>
    </item>
    <item>
      <title>The Codebase You Sit In Rubs Off On You</title>
      <link>https://insight.jatinjainsaraf.com/the-codebase-you-sit-in-rubs-off-on-you</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-codebase-you-sit-in-rubs-off-on-you</guid>
      <description>Your habits as an engineer aren&apos;t shaped by courses and effort alone; they&apos;re shaped by the PRs, teammates, and incidents you sit next to every day.</description>
      <content:encoded><![CDATA[<h1>The Codebase You Sit In Rubs Off On You</h1>
<p>Spend all day at a fish market and you'll leave smelling like fish, even if you never touched one.</p>
<p>Spend your day in a codebase full of shortcuts, and you'll start writing shortcuts too, even if nobody told you to and even if you swore you never would.</p>
<p>That's how environments work. They rub off on you, whether you notice it or not. And in engineering, this isn't a metaphor it shows up in your git log.</p>
<h2>The lie of "it's all about discipline"</h2>
<p>Most engineers think their habits come from training, courses, or sheer effort. If your code is sloppy, the assumption is you didn't try hard enough. If it's clean, you must have "good discipline."</p>
<p>That's only half true. A huge part of how you code comes from what surrounds you every day, on repeat, long before you're conscious of it:</p>
<ul>
<li>The PRs you review, and the ones that get rubber-stamped instead of actually reviewed.</li>
<li>The Slack threads where someone asks "does this really need a test?" and the answer is a shrug.</li>
<li>The postmortems you sit through,  the ones that hunt for a person to blame versus the ones that hunt for a broken process.</li>
<li>The teammate whose code you copy-paste from at 2am because it's the closest example you can find, bugs and all.</li>
</ul>
<p>None of this announces itself as a lesson. Nobody sends a Slack message saying "today I am teaching you to skip error handling." It just seeps in through repetition, the same way an accent seeps in from the people you grow up around.</p>
<h2>Where this actually shows up in production</h2>
<p>This isn't abstract. It shows up as specific, recognizable symptoms:</p>
<p><strong>Silent try/catch blocks everywhere.</strong> Not because anyone taught "swallow your errors," but because the first three examples of error handling you saw in that codebase did exactly that, and you pattern-matched off them without questioning it.</p>
<p><strong>A team where nobody writes tests for "obvious" changes.</strong> Six months later, someone ships an "obvious" one-liner that takes down checkout for twenty minutes. The postmortem calls it human error. It was actually a cultural default nobody chose on purpose, it was absorbed.</p>
<p><strong>Incident channels full of finger-pointing instead of timelines.</strong> New hires learn within a week that admitting a mistake early gets you blamed, so they learn to sit on problems until they're unavoidable. That delay is the expensive part, not the original mistake.</p>
<p><strong>Commit messages that are just "fix," "update," "wip."</strong> You'll find the one senior engineer on the team who started that pattern years ago, still committing "fix" today, and everyone quietly inherited it.</p>
<p>Compare that to a team where the senior engineers write commit messages that explain <em>why</em>, not just <em>what</em>; where someone flags an edge case out loud in standup instead of letting it slide; where an incident review ends with "here's the missing guardrail" instead of "here's who missed it." Sit in that environment for six months and you'll start doing the same things, not because you decided to, but because that's what normal looks like now.</p>
<h2>Why "just work harder" doesn't fix this</h2>
<p>If your habits were purely a function of effort, then two equally hardworking engineers in different environments should converge on similar code quality. They don't. Drop the same disciplined engineer into a team that treats testing as optional, and within a year their testing habits will have eroded, not because they got lazy, but because the environment stopped rewarding the behavior and stopped modelling it.</p>
<p>This is also why "hire good people and leave them alone" doesn't scale culture. Good habits decay under a bad environment faster than bad habits improve under a good one is slow. Osmosis runs both directions, and it runs constantly, not just during onboarding.</p>
<h2>Changing what you're surrounded by is the actual lever</h2>
<p>This is why the fastest way to level up often isn't another course or another book, it's deliberately changing what you're marinating in every day:</p>
<ul>
<li><strong>Review code from engineers better than you, even when it's not required.</strong> You're not looking for bugs; you're absorbing their defaults.</li>
<li><strong>Sit in on incident calls outside your own team.</strong> Watching how a genuinely good incident is run, calm, timestamped, blameless,  recalibrates what "normal" looks like faster than any postmortem template.</li>
<li><strong>Follow people who write and talk publicly about the specific problems you want to get better at.</strong> Your feed is an environment too. It rubs off exactly like your team does.</li>
<li><strong>Choose the side projects and open-source codebases you spend your evenings in as carefully as the one you're paid to work in.</strong> If every codebase you touch after 6pm is a mess, don't be surprised when "mess" starts to feel acceptable at 10am.</li>
</ul>
<p>Your first team teaches you more about "how to engineer" than any style guide, wiki page, or onboarding doc ever will, whether that team is excellent or quietly broken. And it keeps teaching you, every day you stay in it, long after onboarding ends.</p>
<h2>The real question</h2>
<p>The scent of your environment always follows you into your next commit.</p>
<p>So the question worth asking isn't "am I working hard enough?"</p>
<p>It's "what am I letting rub off on me, and is it actually who I want to become as an engineer?"</p>]]></content:encoded>
      <pubDate>Tue, 11 Aug 2026 17:55:26 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Engineering Culture</category>
      <category>Software Engineering</category>
      <category>Career</category>
      <category>Code Quality</category>
    </item>
    <item>
      <title>PostgreSQL&apos;s Async I/O Engine: Why Your Sequential Scans Just Got 2-3x Faster</title>
      <link>https://insight.jatinjainsaraf.com/postgresql-async-io-engine-explained</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/postgresql-async-io-engine-explained</guid>
      <description>For twenty years, every PostgreSQL process read one disk page at a time and waited. PostgreSQL 18 finally lets it ask for many pages at once. Here&apos;s what changed under the hood, and how to verify the gain on your own workload.</description>
      <content:encoded><![CDATA[<p>For as long as PostgreSQL has existed, reading a page from disk has looked the same: a backend process calls <code>read()</code>, the OS schedules the I/O, and the process blocks until the page comes back. Then it asks for the next one. One request, one wait, repeat.</p>
<p>That's fine when your data is sitting in memory. It becomes a real problem on modern NVMe storage. An NVMe drive can have 32 or more I/O requests in flight at once, happily serving them in parallel. Postgres, until version 18, never took advantage of that. It asked for one page, waited for it to come back, then asked for the next. The hardware was capable of a highway; Postgres was driving it one car at a time.</p>
<p>PostgreSQL 18 changes that with a real asynchronous I/O subsystem. Here's the mental model worth keeping: synchronous I/O is a single waiter who takes one table's order, walks it to the kitchen, stands there until it's ready, and only then goes to take the next order. Async I/O is that same waiter dropping ten orders at the kitchen window at once and picking up whichever plates are ready as they come up. Same kitchen, same waiter, dramatically less standing around.</p>
<h2>What actually changed</h2>
<p>Postgres now has a pluggable I/O backend, controlled by a new <code>io_method</code> setting:</p>
<pre><code class="language-ini">io_method = io_uring   # Linux 5.1+, lowest overhead
io_method = worker      # cross-platform fallback (macOS, Windows, older Linux)
io_method = sync        # pre-PG18 behavior
</code></pre>
<p><code>io_uring</code> is the interesting one. It's a Linux kernel interface that gives Postgres two ring buffers shared with the kernel: one to submit I/O requests, one to collect completions. Submitting a batch of reads costs a single syscall instead of one syscall per page. On <code>worker</code>, a pool of background threads makes the blocking calls on your process's behalf so your backend never has to sit and wait itself.</p>
<p>The parameter that actually controls the payoff for read-heavy workloads is <code>effective_io_concurrency</code>. Before PG18 this only affected bitmap heap scan prefetching. Now it controls how many pages ahead a sequential scan, a <code>VACUUM</code>, or a checkpoint will request before it actually needs them:</p>
<pre><code class="language-ini">effective_io_concurrency = 16    # SSD: 16-64
effective_io_concurrency = 200   # NVMe: 64-256
effective_io_concurrency = 2      # HDD: seeks dominate, async barely helps
</code></pre>
<h2>Why this matters in production</h2>
<p>The scenarios where this pays off are the ones that were always I/O-bound: a nightly ETL job doing a full sequential scan of a fact table, <code>VACUUM</code> chewing through a heavily-updated table whose pages aren't in <code>shared_buffers</code>, a checkpoint flushing a large batch of dirty pages, a replication standby trying to flush WAL fast enough to keep lag down.</p>
<p>On paper, a sequential scan of a 10GB table doing 1.3 million page reads at roughly 50 microseconds each costs about 65 seconds fully synchronous. With requests batched 32-deep, that drops toward single-digit seconds in theory. Real workloads don't hit the theoretical ceiling (there's coordination overhead, and the OS page cache intervenes), but a 2-3x throughput improvement on genuinely I/O-bound scans is a realistic, repeatable result, not a marketing number.</p>
<p>The part that matters just as much: if your working set already fits in <code>shared_buffers</code> or the OS page cache, async I/O buys you almost nothing. A cache hit has no I/O latency to overlap in the first place. This isn't a "just turn it on and everything gets faster" feature. It's a fix for exactly one bottleneck: waiting on disk.</p>
<h2>Verifying it on your own workload</h2>
<p>PostgreSQL 18 also ships <code>pg_stat_io</code>, the first built-in view that breaks down I/O by process type and operation. This is how you check whether async I/O is actually doing anything for you, instead of taking it on faith:</p>
<pre><code class="language-sql">SELECT pg_stat_reset_shared('io');

-- run your actual workload here

SELECT
  backend_type,
  reads,
  read_time,
  ROUND(reads::numeric / NULLIF(read_time, 0) * 1000, 0) AS reads_per_second
FROM pg_stat_io
WHERE reads > 1000
ORDER BY reads DESC;
</code></pre>
<p>Run that once with <code>effective_io_concurrency = 1</code> and once with it set to something like <code>64</code>, on the same cold query, and compare <code>read_time</code>. If the number barely moves, your data was already cached and async I/O was never going to help. If it drops meaningfully, that's your workload confirming it was genuinely I/O-bound and the new prefetching is doing real work.</p>
<h2>What it doesn't change</h2>
<p>It's worth being precise about the boundaries. Async I/O doesn't touch MVCC, doesn't change the durability guarantees around WAL flushing, doesn't change lock acquisition, and doesn't reduce the per-connection process overhead that still makes connection pooling mandatory at scale. It's a storage-layer optimization, full stop. Everything above the buffer manager works exactly as it did before.</p>
<p>PostgreSQL isn't getting a new execution model here. It's getting permission to stop waiting in line.</p>]]></content:encoded>
      <pubDate>Sat, 08 Aug 2026 16:23:05 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Performance</category>
      <category>Backend</category>
    </item>
    <item>
      <title>Vector Search in Postgres: What pgvector&apos;s Indexes Actually Cost You</title>
      <link>https://insight.jatinjainsaraf.com/vector-search-in-postgres-what-pgvector-s-indexes-actually-cost-you</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/vector-search-in-postgres-what-pgvector-s-indexes-actually-cost-you</guid>
      <description>Adding a vector column and calling it a day works fine in a demo. In production, it means picking between HNSW and IVFFlat, understanding what &quot;approximate&quot; actually costs you in recall, and watching index build time and memory explode as your embedding table grows. A practical look at pgvector&apos;s index internals and the tradeoffs nobody mentions in the quickstart.</description>
      <content:encoded><![CDATA[<h2>First, What Are We Even Searching?</h2>
<p>Let's ground this in one real example and stick with it the whole way through: you're building a "find similar support tickets" feature. A customer submits a new ticket, and you want to show them five old tickets that are about the same problem, even if they used totally different words to describe it.</p>
<p>To do that, you can't search for matching keywords, because "my payment failed" and "checkout won't go through" mean the same thing but share zero words. So instead, every ticket gets converted into a list of a few hundred (or a few thousand) numbers, called an <strong>embedding</strong>, using an AI model. Two tickets about similar problems end up with number-lists that are mathematically close to each other. Two tickets about unrelated problems end up far apart.</p>
<p>That list of numbers is what Postgres calls a <strong>vector</strong>. "Vector search" just means: given one ticket's list of numbers, find the other tickets whose lists of numbers are closest to it. <code>pgvector</code> is the Postgres extension that lets you store these number-lists in a normal table column and search through them with plain SQL.</p>
<h2>The Part Every Tutorial Skips</h2>
<p>Every "add AI to your app" tutorial has the same steps: install <code>pgvector</code>, add a <code>vector</code> column, run one search query, done. And for a demo with a thousand tickets, that's genuinely all it takes. It just works.</p>
<p>Here's the part tutorials skip: once your support-ticket table grows to a few hundred thousand rows, that same search query, which used to take 8 milliseconds, now takes 900 milliseconds. Someone on your team says "just add an index," like that fixes it the way adding an index fixes a normal slow query. It doesn't, not in the same simple way. Picking the right kind of index means giving up a small amount of accuracy in exchange for speed, and <em>how much</em> accuracy you give up is the whole subject of this post.</p>
<h2>Why a Normal Index Doesn't Work Here</h2>
<p>A normal Postgres index (a B-tree) is built for questions like "give me every ticket where <code>status = 'open'</code>." That's an exact match: a row either satisfies it or it doesn't.</p>
<p>A vector search question is different in kind: "of these 2 million tickets, which five have number-lists closest to this new ticket's number-list?" There's no exact match to look up. Every single row has to have its distance calculated and compared.</p>
<p>Postgres can do this the honest way: calculate the distance from your new ticket to every other ticket in the table, one by one, then sort and keep the closest five. This is called an <strong>exact search</strong>, and it's exactly what your thousand-row demo was quietly doing. At a thousand rows, checking every single one is instant, so you never noticed.</p>
<p><strong>Why this matters in production:</strong> the time this takes grows in a straight line with the number of rows. Double your tickets, double the search time. Forever. There's no clever Postgres trick, no <code>ANALYZE</code>, no <code>EXPLAIN</code> setting that fixes this, because the problem isn't that Postgres is being inefficient. It's that checking every row really is the only way to get a guaranteed-correct answer. The only way to get faster is to stop insisting on a guaranteed-correct answer, and accept "almost certainly correct" instead.</p>
<h2>Two Ways to Cheat (on Purpose): IVFFlat and HNSW</h2>
<p><code>pgvector</code> gives you two index types that both make this trade deliberately: they return an answer that's <em>usually</em> the true closest five tickets, but occasionally misses one, in exchange for being dramatically faster. This is called <strong>approximate nearest neighbor search</strong>, or ANN for short. Think of it like asking a knowledgeable local for the nearest coffee shop instead of checking every coffee shop in the city yourself: almost always right, occasionally not the actual single closest one, but you get an answer in two seconds instead of an hour.</p>
<p>The two options, IVFFlat and HNSW, cheat in different ways, and picking between them is the actual skill here.</p>
<h3>IVFFlat: Sort Tickets Into Labeled Bins First</h3>
<p>Picture a post office that pre-sorts mail into bins by zip code before a carrier ever looks at it. IVFFlat does the same thing to your tickets, before any search happens:</p>
<ol>
<li>When you build the index, Postgres groups all your existing tickets into a fixed number of bins (you choose how many, say 100), based on which tickets' number-lists are similar to each other. Each bin gets a "center point" that represents everything in it, the same way a zip code represents a neighborhood.</li>
<li>When a new ticket comes in and you search, Postgres doesn't check all 100 bins. It only checks a handful of the bins whose center point is closest to your new ticket (you choose how many to check, say 10).</li>
<li>Inside just those 10 bins, it does the slow, exact, check-every-row search, which is now fast because there are far fewer rows to check.</li>
</ol>
<pre><code class="language-sql">-- build the index: sort existing tickets into 100 bins
CREATE INDEX ON support_tickets
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- when searching: only check the 10 closest bins
SET ivfflat.probes = 10;
SELECT id FROM support_tickets
ORDER BY embedding &#x3C;=> '[0.1, 0.2, ...]'
LIMIT 5;
</code></pre>
<p><strong>The catch nobody mentions in the quickstart:</strong> those bins are decided once, on the day you build the index, based on the tickets that existed <em>that day</em>. Six months later, your product has a whole new feature, and a wave of brand-new kinds of tickets start coming in that don't look like anything in your original bins. Postgres still has to put them somewhere, so it shoves each new ticket into whichever old bin is the "least bad fit," even if that bin isn't really a good match. Slowly, quietly, your search results get worse, and there's no error message telling you this is happening. The fix is to periodically rebuild the index (<code>REINDEX</code>) so the bins get redrawn using current data, which on a big table is a real maintenance job, not a background setting you flip once.</p>
<h3>HNSW: Build a Map With Highways and Side Streets</h3>
<p>HNSW works completely differently: instead of bins, it builds a connected map between all your tickets, like a road network, with some very long "highways" connecting far-apart regions and lots of short local roads connecting nearby tickets.</p>
<p>Think about how you'd actually travel from a small town to another small town on the far side of the country. You wouldn't drive the whole way on back roads. You'd take a short local road to a highway, drive the highway most of the way, then take local roads again at the other end. HNSW searches the exact same way: start on the "highway" layer where a few big jumps get you into the right general area fast, then drop down onto smaller, denser layers to fine-tune and land on the actual closest tickets.</p>
<pre><code class="language-sql">-- build the index: construct the layered map, m = how many roads each point connects to
CREATE INDEX ON support_tickets
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- when searching: how wide an area to explore before settling on an answer
SET hnsw.ef_search = 40;
</code></pre>
<p><code>m</code> is roughly "how many roads does each ticket connect to." More roads means better odds of finding the true closest tickets, but the map itself takes up more memory. <code>ef_search</code> is roughly "how much of the map do you explore before giving up and answering." Explore more, and you get a more accurate answer, but it takes longer.</p>
<p><strong>Why this matters in production:</strong> because HNSW never sorts tickets into fixed bins, it doesn't go stale the way IVFFlat does. New kinds of tickets just get woven into the existing map naturally. But that map has real costs. Every single ticket stores a list of which other tickets it's "connected to" by road, and that list has to live somewhere, so <strong>memory use grows with how many roads each point has (<code>m</code>) and how many tickets you have</strong>, not just with how big your rows are. A table that used to fit comfortably in memory as plain rows can suddenly need a lot more RAM once this road-map is layered on top. Also, building this map isn't a quick one-time calculation like a normal index: Postgres has to insert each ticket into the map one at a time, figuring out its connections as it goes. On a table with tens of millions of tickets, that build can take hours, and depending on your Postgres version it can hold a lock that blocks other people from writing to that table the whole time. If tickets are constantly being added, that's a real scheduling problem you need to plan around, not something to discover mid-build.</p>
<h2>Putting Both Side by Side</h2>















































<table><thead><tr><th></th><th>Exact search (no index)</th><th>IVFFlat (labeled bins)</th><th>HNSW (road map)</th></tr></thead><tbody><tr><td>Always finds the true closest matches?</td><td>Yes</td><td>No, usually close</td><td>No, usually close</td></tr><tr><td>Gets slower as the table grows?</td><td>Yes, in a straight line</td><td>Yes, but much more slowly</td><td>Yes, but even more slowly</td></tr><tr><td>Handles brand-new kinds of data well?</td><td>Doesn't matter, always exact</td><td>Poorly, needs periodic rebuilding</td><td>Well, no fixed bins to outgrow</td></tr><tr><td>Cost to build the index</td><td>None</td><td>Cheap, one quick grouping pass</td><td>Expensive, inserts one ticket at a time</td></tr><tr><td>Extra memory needed</td><td>None</td><td>A little, just the bin centers</td><td>More, scales with how many "roads" each ticket has</td></tr><tr><td>Good for tables with constant new writes?</td><td>Fine, just slow</td><td>Accuracy quietly drifts over time</td><td>Handles it well, but writes slow down as the map grows</td></tr></tbody></table>
<p>Neither one is simply "the better index." If your ticket table is built once and doesn't change character much over time, like a fixed product catalog, IVFFlat's cheap build and simple bins are the sensible default. If your table keeps growing and keeps having new, different kinds of data written to it all the time, like a live support-ticket stream, HNSW's steadier accuracy is usually worth its slower, more expensive build.</p>
<h2>The One Question That Actually Matters</h2>
<p>Before shipping either index, the important question isn't "which one is faster," because both are fast enough for almost any real app. The real question is: <strong>how often is it okay for this search to miss the actual best match, and have you ever checked?</strong></p>
<p>That "miss rate" is called <strong>recall</strong>: the percentage of the true best matches your approximate index actually returns. It doesn't show up anywhere. It won't throw an error, and <code>EXPLAIN ANALYZE</code> won't flag it. The only way anyone finds out recall has gotten bad is a vague complaint months later, something like "the suggested tickets don't feel as relevant as they used to," long after the table has grown large enough for the approximation to start visibly slipping.</p>
<p>The fix is simple to say and easy to skip: while your table is still small enough to run a true exact search for comparison, run one, and compare its results against what your approximate index returns. That tells you your real recall number. Do this early, because once the table is big enough that an exact search is too slow to run anymore, you've lost your only way of checking whether the fast index is still giving you good answers.</p>]]></content:encoded>
      <pubDate>Wed, 05 Aug 2026 18:23:39 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>AI</category>
      <category>Backend</category>
      <category>Software Engineering</category>
    </item>
    <item>
      <title>The Three Layers of Failure Isolation: Timeouts, Circuit Breakers, and Load Shedding</title>
      <link>https://insight.jatinjainsaraf.com/the-three-layers-of-failure-isolation-timeouts-circuit-breakers-and-load-shedding</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-three-layers-of-failure-isolation-timeouts-circuit-breakers-and-load-shedding</guid>
      <description>500 requests a second are hitting a dependency that is completely dead. Every one gets a clean timeout after 2 seconds, exactly as configured. And you are still down. Three patterns, stacked in order, timeouts and retries, circuit breakers and bulkheads, backpressure and load shedding, each one picking up exactly where the last one&apos;s guarantee runs out.</description>
      <content:encoded><![CDATA[<blockquote>
<p>500 requests a second are hitting a dependency that is completely dead. Every single one gets a clean timeout after 2 seconds, exactly as configured. And you are still down.</p>
</blockquote>
<p>That sentence is the whole problem with treating resilience as a single setting. A timeout did its job, nothing hung forever, and the aggregate is still an outage, because you're holding 1,000 concurrent doomed requests at once, and every one of them is load on a dependency that's trying to restart.</p>
<p>There isn't one pattern that fixes this. There are three, stacked, each one picking up exactly where the last one's guarantee runs out. Get the order wrong, or skip one, and you don't get partial protection, you get a different failure mode wearing the same symptoms.</p>
<p>This is that stack: timeouts and retries, circuit breakers and bulkheads, backpressure and load shedding. What each one actually bounds, why the one before it isn't enough, and the specific way each gets misconfigured in a way that looks fine until the day it doesn't.</p>
<h2>Layer One: A Timeout Doesn't Wait Patiently, It Fails Together</h2>
<p>Most client libraries default to no overall timeout. <code>fetch</code> in Node has no total deadline unless you add one. Plenty of database drivers wait indefinitely. That default isn't neutral, it's a decision to couple your availability to your slowest dependency, and Little's Law explains exactly how much:</p>
<pre><code>concurrency = arrival rate × service time
</code></pre>
<p>A dependency's p99 goes from 50ms to 30 seconds. Your arrival rate hasn't changed. Your concurrency just rose by a factor of 600, and every one of those in-flight requests is holding a socket, a pool connection, a request slot, memory. At 200 requests/second and a 30-second service time, you need 6,000 concurrent slots. You don't have 6,000.</p>
<pre><code>downstream slows → your concurrency climbs → pool exhausted → queue builds
                 → YOUR p99 rises for every endpoint, including ones that
                   never call that dependency → your own callers time out
                 → and they retry, adding load → you are now the outage
</code></pre>
<p>Nothing failed. A dependency got slow, and the absence of a bound propagated it outward. A timeout is how you convert <em>someone else's</em> latency problem into <em>your</em> error rate, which sounds like a downgrade and isn't, because an error is bounded and recoverable, while unbounded latency spreads.</p>
<blockquote>
<p><strong>The queue at a single service desk, where one customer's transaction is taking forty minutes.</strong> With no policy, the twenty people behind them wait forty minutes, the next twenty leave, and the shop's throughput collapses over one difficult case. With a policy, "five minutes per customer, then take a ticket and come back", one customer is inconvenienced and the queue keeps moving.</p>
</blockquote>
<h3>Only one of your four timeouts actually bounds anything</h3>
<p>A single number labelled "timeout" is usually not the number you think it is:</p>
<ul>
<li><strong>Connect</strong> bounds the TCP handshake. Catches a host that's down or unroutable.</li>
<li><strong>Time to first byte</strong> bounds server thinking time. Catches a slow query or a saturated server.</li>
<li><strong>Idle / socket</strong> bounds the gap between bytes. Catches a stream that stalls mid-response.</li>
<li><strong>Total / overall</strong> bounds the whole operation, including retries. <strong>This is the only one that actually protects your resource usage.</strong></li>
</ul>
<p>A 2-second connect timeout and a 5-second read timeout don't add up to a guarantee, a response trickling one byte every 4 seconds never trips either one. Set the overall deadline, always, and treat the other three as diagnostics that let you fail earlier with a clearer reason why.</p>
<p>Choose the number from the <strong>p99.9 of the successful response distribution</strong>, not the average. Too short and you abandon requests that would have succeeded, converting them into retries, a load amplifier disguised as a safety measure. Too long and you hold resources through a failure you could have detected sooner.</p>
<p>And here's the arithmetic that catches nearly everyone:</p>
<pre><code>Your caller's timeout:      5s
Your per-attempt timeout:   3s
Your retry policy:          3 attempts
Your worst-case total:      3 + backoff + 3 + backoff + 3 ≈ 10s

At t=5s your caller already gave up. Attempts 2 and 3 are work
for nobody, executed against a dependency that's already struggling.
</code></pre>
<p>A retry policy has to fit inside the overall budget: per-attempt timeout is <code>remaining_budget / max_attempts</code>, not a number picked independently.</p>
<h3>Not every failure is safe to retry, and a status code isn't proof</h3>
<p>Connection refused, DNS failure, 503, 502, safe to retry, nothing happened yet. A 429, safe, and honour <code>Retry-After</code>. A 500 or a timeout, <strong>safe only if the operation is idempotent</strong>, because both are the canonical ambiguous failure: the request may have already succeeded server-side and you just never heard back. A 400, 422, 404, never retry; identical bytes fail identically.</p>
<p>HTTP method semantics are a hint, not a guarantee. <code>GET</code> and <code>PUT</code> are <em>specified</em> idempotent, and plenty of real handlers aren't, a <code>GET</code> that increments a view counter, a <code>PUT</code> that appends. Decide from what the operation does and whether it carries an idempotency key, not from the verb.</p>
<h3>Backoff needs jitter, and jitter isn't a refinement</h3>
<p>Fixed-interval retries fail for a specific reason: a thousand clients that failed at the same moment retry at the same moment.</p>
<pre><code>Fixed 1s:      ████    ████    ████     ← full fleet, in phase, forever

Exponential:   ████      ████        ████    ← spaced out, still in phase

Exp + jitter:  ▁▃▂▁▄▂▃▁▂▄▁▃▂▄▁▂▃▁▄▂▃  ← smeared into a manageable trickle
</code></pre>
<p>Exponential backoff fixes the frequency of retries. It does nothing for synchronisation. Without jitter, a recovering dependency gets knocked over by the first aligned burst, which restarts the whole cycle.</p>
<pre><code class="language-typescript">// Full jitter: sleep is uniform over [0, capped exponential]
const backoff = (attempt: number) => {
  const exp = Math.min(30_000, 200 * 2 ** attempt);   // cap matters, uncapped, attempt 12 waits 9 hours
  return Math.random() * exp;
};
</code></pre>
<p>Cap the exponential, bound the attempt count, and make the first retry near-immediate for a plain connection refusal, that failure costs the dependency nothing to answer again.</p>
<h3>Retry amplification: the incident that outlives its own cause</h3>
<p>Here's the part that turns a thirty-second blip into a two-hour outage.</p>
<p>Retries multiply through layers. A request path where every layer retries three times:</p>
<pre><code>client ──3×──► gateway ──3×──► API ──3×──► service ──3×──► database
                                                  81 attempts at the bottom
                                                  for ONE user request
</code></pre>
<p>Each layer is individually reasonable. Together they're an 81× amplifier, and it engages <em>exactly when the deepest component is failing</em>, because that's what triggered the retries in the first place. A database at a 50% error rate, hit with three times its normal load, doesn't recover. It produces more errors. Which produce more retries. Which produce more errors.</p>
<p>This is a <strong>metastable failure state</strong>: one that sustains itself after its trigger is gone. A 30-second network blip causes a wave of retries. The retries push load above capacity. Being above capacity produces timeouts, which produce more retries. The network has been fine for an hour, and the system does not recover, because the load keeping it down is the load its own failure generated. Removing the original cause changes nothing. The only way out is reducing load: shedding traffic, opening circuit breakers, or manually pulling the service out of rotation until queues drain.</p>
<p>Two rules follow from this. <strong>Retry at one layer, not at every layer</strong>, pick the one closest to the business logic, the one that holds the idempotency key, and make every other layer pass failures straight through. If a service mesh retries for you, the application must not also retry, or you've silently rebuilt the multiplier.</p>
<p>And bound it structurally with a <strong>retry budget</strong>, this is the single most valuable thing in this entire layer, and the mechanism most systems lack:</p>
<pre><code class="language-typescript">// Allow retries only up to ~10% of successful request volume.
// Errors can spike without retry load spiking, that's the whole point.
if (retryTokens.tryConsume()) { /* attempt */ }   // token bucket, refilled by SUCCESSES
</code></pre>
<p>With a per-request retry count, a 100% error rate produces 3× or 81× load. With a 10% retry budget, that same 100% error rate produces <strong>1.1× load</strong>, the system fails fast and cheap, and leaves the dependency enough headroom to actually recover.</p>
<p><strong>Why this matters in production:</strong> the trigger for these incidents is almost always mundane, a failover, a deploy, a brief partition. The <em>duration</em> is set entirely by whether the system can shed the load its own retries created. A retry budget and a circuit breaker cost about an afternoon each to build, and they're the difference between a five-minute blip and a two-hour incident.</p>
<h3>Cancellation has to actually cancel</h3>
<p>A client-side timeout does not stop the server. This is the detail that surprises people the most, and it matters most at the database:</p>
<blockquote>
<p>A Node query timeout abandons the <em>response</em>. The Postgres backend keeps executing the query, holding the connection, the snapshot, and its locks.</p>
</blockquote>
<p>So a client-side timeout on a slow query gives you the worst of both worlds, the client has moved on and may retry, doubling the work, while the server runs the original query to completion anyway. Enforcement has to happen server-side:</p>
<pre><code class="language-sql">SET statement_timeout = '2s';                       -- the actual bound
SET lock_timeout = '1s';                            -- don't queue behind a lock
SET idle_in_transaction_session_timeout = '10s';    -- kill abandoned transactions
</code></pre>
<h2>Layer Two: A Breaker Stops Making the Calls At All</h2>
<p>Layer one bounds each call. It does not stop you from making a thousand doomed calls a second.</p>
<p>Go back to that dependency that's genuinely down, 2-second timeout, 500 requests/second arriving:</p>
<pre><code>500 req/s × 2s each = 1,000 concurrent in-flight requests
                      ...all of which will fail
                      ...each holding a socket, a slot, and memory
                      ...and all of it is load on a dependency trying to restart
</code></pre>
<p>The timeout did its job. Each request failed in bounded time. The aggregate is still an outage, you're spending your entire concurrency budget discovering, five hundred times a second, something you already knew.</p>
<p>A <strong>circuit breaker</strong> is the observation that after enough failures, you can just stop asking. It converts a 2-second failing call into a sub-millisecond local rejection, and that does three things at once: it frees your resources instantly, it removes load from the dependency, giving it room to actually recover, which is the exit from the metastable state above, and it fails fast enough that a fallback becomes viable. A 2-second wait before serving a cached value is a bad experience. A 1-millisecond rejection followed by a cache read is a fine one.</p>
<p>That second point is easy to undervalue and is often the decisive one. A dependency at 100% error rate does not recover while it's still receiving full traffic plus retries. A breaker is how the traffic actually stops, without a human doing it by hand.</p>
<blockquote>
<p><strong>The electrical breaker the pattern is named after</strong> doesn't protect the appliance that shorted, that appliance is already broken. It protects the rest of the house, by isolating the one circuit so the wiring doesn't overheat and every other room keeps its lights on. And it needs a delay before reclosing, because closing it immediately onto an unfixed short achieves nothing but another trip.</p>
</blockquote>
<h3>The four ways a breaker gets misconfigured</h3>
<p><strong>Trigger on a failure rate over a rolling window, not consecutive failures.</strong> A consecutive-failure counter fails in both directions, on a low-traffic endpoint, five consecutive failures might span twenty minutes and open a breaker for a problem long since resolved, and on a dependency where half of calls succeed, a consecutive counter never reaches five at all. Use a rate with a minimum-volume guard, so a single failure after a quiet period doesn't read as a "100% failure rate over a sample of one":</p>
<pre><code class="language-typescript">const stats = window.last(10_000);                 // last 10 seconds
if (stats.total >= 20 &#x26;&#x26; stats.failureRate > 0.5) open();
</code></pre>
<p><strong>A <code>4xx</code> is not a failure.</strong> This is the misconfiguration that causes the most damage in practice. A 400, 404, or 422 means the dependency is healthy and rejected your request, bad input, a missing resource, failed validation. Count those as breaker failures and a single client bug, a URL scanner, or one malformed integration takes down a perfectly healthy dependency for every other caller.</p>
<p><strong>Threshold on slow calls too, not just errors.</strong> A dependency at 100% success and 8 seconds per call is doing just as much damage as one that's down, arguably more, because nothing about it looks like an error. A slow-call-rate threshold ("if 60% of calls exceed 1 second, open") is the check most implementations skip entirely.</p>
<p><strong>Half-open must admit a trickle, not a flood.</strong> A cooldown that expires and lets the full 500 requests/second back through at once re-hammers a recovering dependency and reopens the breaker instantly. Admit one to three concurrent probes, require a few successes before closing, and ideally ramp: half-open at 5% of traffic, then 25%, then closed.</p>
<h3>Bulkheads: stop one dependency from starving everything else</h3>
<p>A ship's hull is divided into watertight compartments so one breach floods a single compartment, not the vessel. The system equivalent: partition the resource requests compete for, so one dependency's slowness can't consume all of it.</p>
<pre><code>Without: 200 concurrent slots, shared.
  The recommendations service slows to 8s. Requests to it accumulate.
  Within seconds all 200 slots hold recommendation calls.
  Checkout, which never calls recommendations, gets no slot. Total outage.

With:  recommendations capped at 20 concurrent.
  Slot 21 onwards is rejected immediately → the fallback runs.
  Checkout's 180 slots are untouched. Recommendations are degraded. Nothing else is.
</code></pre>
<p>In Node there's no thread pool to partition, which leads some people to conclude bulkheads don't apply. They do, the shared resource is event-loop time, the socket pool, and memory held by pending promises. The mechanism is a semaphore that <strong>rejects when full</strong>, not one that queues without bound:</p>
<pre><code class="language-typescript">import pLimit from "p-limit";
const limits = {
  recommendations: pLimit(20),     // optional feature: small allowance
  payments:        pLimit(60),     // critical path: generous
};

async function callRecommendations(userId: string) {
  if (limits.recommendations.pendingCount > 50) throw new BulkheadFull();
  return limits.recommendations(() => fetchRecs(userId));
}
</code></pre>
<p>A semaphore that queues indefinitely isn't a bulkhead, it's a delay, and the memory held by that queue is exactly the resource you were trying to protect. Size it from Little's Law: a dependency with a 50ms p99 sustaining 200 requests/second needs 10 concurrent slots, not 200. And the single highest-value bulkhead most teams skip is separate connection pools per workload, web traffic and a reporting job should never draw from the same Postgres pool.</p>
<h3>Degradation is design work, and it hides the failure that follows</h3>
<p>Breakers and bulkheads decide <em>when</em> to stop calling something. Degradation decides <em>what the user gets instead</em>, and it's the part that can't be configured away, it needs a decision per dependency, often a product decision rather than an engineering one.</p>
<p>The rule that catches the most bugs: <strong>the fallback must not depend on what failed.</strong></p>
<pre><code class="language-typescript">// BROKEN: the fallback path hits the same overloaded database.
try   { return await cache.get(key); }
catch { return await db.expensiveQuery(); }   // ← 100% of traffic now goes here
</code></pre>
<p>Redis going down doesn't just remove the cache, it redirects the entire cache hit rate onto the database. A cache at a 95% hit ratio failing means the database sees 20× its normal read load, and the fallback has just converted a cache outage into a database outage. The correct version serves the degraded path behind a bulkhead and a request-coalescing lock, and sheds the excess rather than manufacturing a second failure.</p>
<p>And here's the trap specific to this layer, worth sitting with: <strong>degradation removes failure from your error rate.</strong> Once it's working, a failing dependency produces no errors. Requests succeed. Latency might even improve, because a fast fallback replaced a slow call. Your dashboard is flat. Your alerts are quiet.</p>
<p>So a system can serve the generic homepage instead of the personalised one for three weeks, because a breaker opened after a deploy changed a hostname and never closed, and nobody notices, because nothing is red. The first report comes from a product manager asking why engagement dropped. The fix is instrumenting the thing degradation hides: breaker state as a metric with an alert on "open for longer than N minutes," and a degraded-serve rate tracked as its own SLI, right next to your error rate, because it <em>is</em> the error rate you decided not to show users.</p>
<h2>Layer Three: When Nothing Is Broken and You're Still Down</h2>
<p>Layers one and two both answer the same question: <em>a dependency I call is broken, what do I do?</em> This layer answers a different one entirely.</p>
<blockquote>
<p>Everything downstream is healthy. Every query is fast. And 9,000 requests a second are arriving at a service that can serve 6,000.</p>
</blockquote>
<p>No breaker opens, because nothing is failing. No bulkhead helps, because no single dependency is being monopolized, the resource is being consumed by legitimate, healthy work. Queues just grow, latency climbs uniformly across everything, and eventually it all times out at once.</p>
<p>The counterintuitive fact underneath this: capacity isn't a ceiling you bump into. It's a knee, after which things get <em>worse</em>, not just full. <strong>Throughput</strong> is requests completed per second. <strong>Goodput</strong> is requests completed per second that anybody still wanted, inside the caller's deadline. Past the knee, throughput can look almost flat while goodput falls off a cliff, because the work still being completed is work whose caller gave up on seconds ago. A service can sit at 100% CPU, complete 6,000 requests a second, and deliver a genuinely useful 400, and its throughput graph will look perfectly fine the entire time.</p>
<blockquote>
<p><strong>A kitchen that takes every order the floor brings.</strong> At 30 covers it's fine. At 90, tickets pile up, and by the time each dish is plated the table has left. The kitchen is working flat out, food is going out at the maximum rate the stoves allow, and nobody is eating it. The fix isn't a bigger ticket rail, it's the maître d' at the door saying "we're full, forty-five minute wait." That refusal is the only intervention that gets the guests who <em>are</em> seated actually fed.</p>
</blockquote>
<h3>An unbounded queue is not a buffer</h3>
<p>"Add a queue so we can absorb the spike" is correct advice for a transient burst and catastrophic advice for sustained overload. The arithmetic is Little's Law again:</p>
<pre><code>queue depth 50,000, throughput 500/s → wait time = 100 seconds
</code></pre>
<p>Every one of those 50,000 items gets processed eventually. Every single one gets processed after its caller has already timed out. The system does the full amount of work and delivers none of the value, and the memory holding that queue is itself now a failure mode.</p>
<p>Worse, the queue changes the <em>shape</em> of the failure into a less useful one. Without it, request 6,001 gets an immediate <code>503</code>, the client knows right away, can retry with backoff, can fall back, can tell the user something. With an unbounded queue, request 6,001 gets accepted and times out 30 seconds later, after the client has waited and held its own resources for nothing, learned nothing sooner, and now retries, adding a <em>second</em> item to the queue for work you'd already started.</p>
<p>So: every queue is bounded, and the bound comes from the latency target you actually care about.</p>
<pre><code>max_depth = target_latency × service_rate
</code></pre>
<p>Serving at 500/s with a 2-second latency target means a queue of 1,000 items, not one more. Beyond that, reject. This applies to your HTTP accept queue, your connection-pool wait queue, your job queue, every in-process channel. An unbounded queue anywhere in the request path is exactly where the latency will accumulate.</p>
<h3>Backpressure where you can, shedding where you can't</h3>
<p>Backpressure means the consumer tells the producer to slow down, and the producer <em>can</em>. It's strictly better wherever it's available, because no work gets discarded, the rate just matches. TCP flow control is the original version of this idea; HTTP/2 flow-control windows, reactive streams, bounded channels where the producer blocks, and consumer pause/resume on a Kafka worker are all re-implementations of it. In Node, <code>writable.write()</code> returning <code>false</code> <em>is</em> backpressure, ignoring that return value and writing anyway is the standard way to leak memory in a pipeline.</p>
<p>Backpressure works inside a closed system, your own pipeline, code you control. It fails completely at an open boundary: you can't tell a million browsers, a partner's integration, or a mobile app fleet to send fewer requests. There's no window to shrink. At that boundary, the only option left is <strong>load shedding</strong>, refuse work immediately and cheaply, so the work you do accept actually completes. Shedding isn't a failure of design. It's the design. The choice was never "shed or serve everyone", it's "shed deliberately, or let the overload choose for you by timing everything out instead."</p>
<h3>Shed on the signal that tells the truth earliest</h3>
<p>Not every signal is equally honest, and the ranking matters:</p>
<p>Queue wait time is the best signal, it <em>is</em> the latency you're about to violate, and it leads everything else. Concurrency in flight versus your limit is next, direct and cheap. Queue depth is good but needs a service rate to interpret. Event-loop delay in Node measures the actual contended resource. CPU utilisation is mediocre, saturation isn't linear in CPU, and I/O-bound work can show low CPU while queueing badly. Latency p99 lags, by the time it moves, the queue is already deep. And error rate is the worst signal of all: it's the outcome you were trying to prevent in the first place, arriving last.</p>
<p>For Node specifically, event-loop delay is an excellent, cheap, local signal:</p>
<pre><code class="language-typescript">import { monitorEventLoopDelay } from "node:perf_hooks";
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
// Shed when the loop is persistently behind: the process cannot keep up, full stop.
const overloaded = () => h.mean / 1e6 > 70;   // ms
</code></pre>
<p>And a shed response is only cheap if it happens early, reject before authentication, before deserialising a large body, before touching the database. A <code>503</code> that costs as much to produce as a <code>200</code> protects nothing at all.</p>
<h2>The Order Matters, Not Just the Presence</h2>
<p>Put together, the three layers cover three genuinely different failures, and none of them substitutes for another:</p>
<p>A <strong>timeout</strong> bounds one call to a dependency that's slow. It does nothing about the <em>volume</em> of calls you keep making to one that's dead, that's what a <strong>circuit breaker</strong> stops. A breaker does nothing about legitimate, healthy traffic simply exceeding your own capacity, that's <strong>backpressure and shedding</strong>. And underneath all three sits the retry logic that, misconfigured, turns any one of these into a self-sustaining outage regardless of how well the other two are built.</p>
<p>This is also, not coincidentally, the fuller version of a claim made in passing in an earlier piece on <a href="https://insight.jatinjainsaraf.com/connection-pooling-in-the-serverless-era-five-failure-modes">connection pooling in serverless environments</a>: that platform retry policies, re-invoking every failed serverless request during a connection exhaustion event, are "one of the few failure modes where client retries are strictly harmful." That's retry amplification, in exactly the shape described above, a thousand concurrent invocations failing at once, each one retried by the platform, adding load to a connection table whose entire problem was already too many clients. A retry budget at the right layer, or a breaker that stops the calls outright, is the actual fix; raising <code>max_connections</code> again is not.</p>
<p>None of these three layers is optional once you're running anything with a real dependency graph. But they answer different questions, they fail in different ways when misconfigured, and the order in which you reach for them, bound the call, stop the calls, then shed what you can't backpressure, is the order that actually holds under load.</p>
<hr>
<p><em>Sourced from the <a href="https://academy.jatinjainsaraf.com/system-design-in-depth">System Design In-Depth</a> course, <a href="https://academy.jatinjainsaraf.com/system-design-in-depth/timeouts-retries-backoff">Timeouts, Retries, and Backoff</a>, <a href="https://academy.jatinjainsaraf.com/system-design-in-depth/circuit-breakers-bulkheads-degradation">Circuit Breakers, Bulkheads, and Graceful Degradation</a>, and <a href="https://academy.jatinjainsaraf.com/system-design-in-depth/backpressure-and-load-shedding">Backpressure and Load Shedding</a>.</em></p>]]></content:encoded>
      <pubDate>Mon, 03 Aug 2026 17:53:46 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>System Design</category>
      <category>Resilience Engineering</category>
      <category>Distributed Systems</category>
      <category>Backend</category>
      <category>Architecture</category>
    </item>
    <item>
      <title>Connection Pooling in the Serverless Era: Five Failure Modes</title>
      <link>https://insight.jatinjainsaraf.com/connection-pooling-in-the-serverless-era-five-failure-modes</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/connection-pooling-in-the-serverless-era-five-failure-modes</guid>
      <description>Your database CPU is at 20%, your slowest query is 12ms, your slow-query log is empty, and your p99 is three seconds. Five ways connection pooling breaks in serverless and autoscaled deployments, why each one disguises itself as something else, and what every fix actually costs.</description>
      <content:encoded><![CDATA[<blockquote>
<p>Your database CPU is at 20%. Your slowest query is 12ms. Your slow-query log is empty. And your p99 is three seconds. This is what a connection pool failure looks like — and it never looks like a database problem.</p>
</blockquote>
<p>Every engineer learns the same sentence about connection pooling: "reuse connections instead of opening a new one per request." It's true, it's useful, and it's where most people stop.</p>
<p>Then you deploy to serverless. Or you turn on autoscaling. Or someone adds a metrics exporter. And you discover that connection pooling isn't a performance optimisation you bolt on — it's a shared, global, hard-capped budget that half your infrastructure is spending without telling anyone.</p>
<p>This article is about the failure modes. Not "what is a connection pool," but the five specific ways pooling breaks in modern deployments, why each one disguises itself as something else, and what the fix actually costs you.</p>
<hr>
<h2>First: A Postgres Connection Is Not a Socket</h2>
<p>Engineers price a database connection like an HTTP connection — a file descriptor, some buffers, a few kilobytes. Cheap. Open a thousand of them.</p>
<p>In PostgreSQL, a connection is a <strong>forked operating system process</strong>. Per connection, you get:</p>
<p>An OS process with its own page tables and scheduler entry, plus a few megabytes of private memory that never becomes shared. This is why connecting to Postgres costs orders of magnitude more than connecting to Redis, and why "one connection per request" is a design error rather than a style preference.</p>
<p>The right to allocate <code>work_mem</code> — and not once per connection, but <strong>per sort or hash node in the running query</strong>. One query with three hash joins holds three multiples of it. This is how databases get OOM-killed while every dashboard looks calm.</p>
<p>A snapshot, if the connection is inside a transaction, which joins the vacuum horizon. That's how a single forgotten <code>idle in transaction</code> connection blocks dead-tuple cleanup across the entire database.</p>
<p>So <code>max_connections = 100</code> isn't an arbitrary cap the Postgres developers picked to annoy you. It's <strong>a memory and scheduler budget expressed as a count</strong>. Raising it to 2,000 doesn't buy you 2,000 connections' worth of throughput — it buys you 2,000 processes contending for the same cores and the same <code>shared_buffers</code>, trading a polite refusal of the 101st client for death by memory pressure.</p>
<blockquote>
<p><strong>The analogy worth keeping:</strong> your database is a restaurant with a fixed number of tables and a fixed number of cooks. <code>max_connections</code> is the tables. On a busy night the tempting move is to cram in more tables — but the cooks didn't multiply, so every dish arrives late and the kitchen falls behind on all of it at once. The restaurant that keeps ten tables and queues everyone else at the door serves <em>more</em> diners per hour.</p>
<p>That queue at the door is your connection pool's wait queue. It is the cheapest place in your entire system for a request to wait, and the last place anyone thinks to measure it.</p>
</blockquote>
<hr>
<h2>Failure Mode 1: The Only Number That Matters Is <code>N × Pool Size</code></h2>
<p>A pool is configured <strong>per process</strong>. The limit is <strong>global</strong>. Nearly every exhaustion incident lives in that gap.</p>
<p>Here's a realistic accounting for a modest production system running against <code>max_connections = 100</code>:</p>


















































<table><thead><tr><th>Consumer</th><th>Count</th><th>Connections</th></tr></thead><tbody><tr><td>API instances × pool</td><td>12 × 20</td><td>240</td></tr><tr><td>Worker instances × pool</td><td>4 × 10</td><td>40</td></tr><tr><td>Cron / scheduled jobs</td><td>3</td><td>3</td></tr><tr><td>Migration job during deploy</td><td>1</td><td>1–5</td></tr><tr><td>Metrics exporter</td><td>2</td><td>2–10</td></tr><tr><td>An analyst's <code>psql</code> session</td><td>1</td><td>1</td></tr><tr><td><code>superuser_reserved_connections</code></td><td>—</td><td>3 reserved</td></tr><tr><td><strong>Total demand</strong></td><td></td><td><strong>~290 against 97 usable</strong></td></tr></tbody></table>
<p>Two things make this vicious.</p>
<p><strong>It's a deploy-time cliff, not a load-time one.</strong> A rolling deploy briefly runs old and new instances side by side, doubling that top row. Which is why these incidents correlate with deploys rather than with traffic, and why the postmortem keeps looking at the wrong graph.</p>
<p><strong>The bottom rows are invisible.</strong> Nobody counts the metrics exporter. Nobody counts the migration container. Those are exactly what tip you over.</p>
<p>The rule, stated properly: <strong>allocate <code>max_connections</code> as a budget across all consumers, then derive per-instance pool size by division.</strong></p>
<pre><code>pool_size_per_instance = floor(max_connections / max_instances) - safety_buffer
</code></pre>
<p>And the cost of that rule, stated honestly: pool size now depends on replica count. It has to be computed against your <strong>autoscaler's ceiling</strong>, not your current instance count — which means you deliberately run a pool smaller than any single instance could use at peak.</p>
<p><strong>Why this matters in production:</strong> your autoscaler's max-replica setting <em>is</em> a database configuration setting. If <code>max_replicas × pool_size</code> exceeds <code>max_connections</code>, you haven't got a risk. You've configured an outage and scheduled it for the next traffic spike.</p>
<hr>
<h2>Failure Mode 2: Serverless Turns Concurrency Into Connections</h2>
<p>A function runtime has no shared pool because it has no shared process. Each concurrent invocation is an isolated environment whose pool has a maximum useful size of one — it serves exactly one request.</p>
<p>Warm containers reuse a connection across invocations, which helps, and which is precisely why this problem is intermittent and maddening to reproduce. But the scaling unit is <strong>concurrency</strong>, and concurrency is the one thing you don't control.</p>
<pre><code>1,000 concurrent invocations × 1 connection each = 1,000 connections
max_connections = 100, minus 3 reserved, minus everything in the table above
</code></pre>
<p>Roughly 90 invocations get a connection. The rest get <code>FATAL: sorry, too many clients already</code>.</p>
<p>There's a specific version of this that catches teams who <em>did</em> read the tutorial:</p>
<pre><code class="language-javascript">// Looks correct. Is correct — in a long-running Node server.
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
});

export default async function handler(req, res) {
  const result = await pool.query('SELECT * FROM transactions LIMIT 10');
  res.json(result.rows);
}
</code></pre>
<p>In a long-running process, <code>const pool</code> is created once and reused across every request. Correct. In a serverless function, the module is re-imported on each cold start — so <code>max: 10</code> isn't a ceiling of 10 connections, it's a ceiling of <strong>10 per concurrent environment</strong>. A hundred concurrent invocations makes it 1,000.</p>
<p>The global singleton pattern helps at the margins, because it survives warm starts within a container:</p>
<pre><code class="language-typescript">// lib/db.ts
import { Pool } from 'pg';

const globalForPg = global as typeof global &#x26; { pgPool?: Pool };

if (!globalForPg.pgPool) {
  globalForPg.pgPool = new Pool({
    connectionString: process.env.DATABASE_URL,
    max: 2,                      // small per instance — the pooler owns the total
    idleTimeoutMillis: 10_000,
    connectionTimeoutMillis: 5_000,
  });
}

export const pool = globalForPg.pgPool;
</code></pre>
<p>But be clear about what this is: <strong>damage control, not a fix.</strong> It caps each environment at 2 connections instead of 10. It does not stop the platform from giving you 500 environments. On Vercel Functions and equivalents, the singleton pattern alone is insufficient — every team shipping a direct Postgres connection plus a singleton is running on borrowed time, and hasn't hit the limit only because they haven't hit enough concurrent traffic yet.</p>
<h3>The blast radius is the real story</h3>
<p>What makes this an architecture problem rather than a tuning problem is that <strong>the failure does not land on the traffic that caused it.</strong></p>
<p>The connection table is global. When it's full, it's full for everybody:</p>
<ul>
<li>The internal admin panel stops loading — and its on-call isn't yours.</li>
<li>The payouts worker's queue backs up silently.</li>
<li>The metrics exporter fails, so your dashboards go blank <em>during</em> the incident.</li>
<li>An engineer opens <code>psql</code> to investigate and gets refused.</li>
</ul>
<p>Then the platform's retry policy re-invokes every failed request, adding load to a resource whose entire problem is too many clients. This is one of the few failure modes where <strong>client retries are strictly harmful</strong>.</p>
<p><strong>Why this matters in production:</strong> connection exhaustion is a tenancy problem as much as a capacity one. If one spiky autoscaled workload can consume the budget of your payments worker, you've coupled two services through a resource neither of them monitors. The cheap mitigation is a per-role cap — <code>ALTER ROLE app_web CONNECTION LIMIT 40</code> — which converts a shared outage into a contained one, at the cost of one workload hitting its ceiling while slots sit idle elsewhere.</p>
<hr>
<h2>Failure Mode 3: Raising the Pool Makes It Worse</h2>
<p>This is the counterintuitive one, and it's the standard incident response.</p>
<p>Requests are timing out waiting for connections. An engineer raises the per-instance pool from 20 to 100 across 10 instances, restarts, and throughput drops further while p99 gets worse.</p>
<p>Little's Law explains why. <code>L = λW</code> — the number of connections you need busy at once is arrival rate times hold time. Take 400 req/s where each request runs one 5ms query, then run it again after a bad plan pushes that query to 200ms:</p>
<pre><code>Healthy:  L = 400/s × 0.005s = 2 connections busy on average
          ρ = (400 × 0.005) / 20 = 0.1   → waits negligible

Degraded: L = 400/s × 0.200s = 80 connections wanted, against a pool of 20
          ρ = (400 × 0.200) / 20 = 4.0   → demand exceeds capacity, queue grows unbounded
</code></pre>
<p>Two connections is the honest answer at healthy load. Intuition wants pool size to track concurrent <em>requests</em>; Little's Law says it tracks concurrent requests <strong>× the fraction of their life spent inside the database</strong>, which is usually small. That gap is why correctly-sized pools look absurdly low to people who haven't done the arithmetic.</p>
<p>So why doesn't a bigger pool help in the degraded row? Because service time <code>S</code> isn't a constant. It depends on how many queries are executing concurrently against fixed cores and a fixed disk. Past hardware saturation, extra in-flight queries add no throughput — they divide the same throughput into slower pieces. Then second-order costs push throughput actively <em>down</em>: context switching between hundreds of runnable backends, contention on buffer-mapping and lock-manager partitions, <code>work_mem</code> allocations evicting your working set from cache, and more concurrent snapshots holding back the vacuum horizon.</p>
<p>Hence the principle worth memorising: <strong>queueing outside the database is nearly free; queueing inside it is expensive.</strong> A request in a pool's FIFO costs a promise and a timer. A request inside the database costs a process, several megabytes, a snapshot, and a share of every lock partition it touches.</p>
<p>The community starting point, popularised by HikariCP, is <code>connections ≈ (2 × core_count) + effective_spindles</code>. Treat it as a hypothesis to load-test, <strong>not a law</strong> — <code>effective_spindles</code> is a rotational-disk-era proxy for storage concurrency, and on NVMe at 500K+ random IOPS it means something quite different from its name. Note also that it lands in the low tens <em>for the whole database</em>, not per instance.</p>
<p><strong>Why this matters in production:</strong> during pool exhaustion, the right move is almost never "raise the pool." It's to find what raised hold time — a slow query, a lock wait, an external call inside a transaction — because <code>L = λW</code> makes pool demand linear in <code>W</code>, and <code>W</code> is the term you can usually cut by an order of magnitude. Raising <code>C</code> to match a broken <code>W</code> just relocates the collapse into the database, where it costs more and hides better.</p>
<hr>
<h2>Failure Mode 4: Transaction Pooling Silently Eats Session State</h2>
<p>The structural fix for serverless is a transaction-mode pooler — PgBouncer, RDS Proxy, Supavisor, Prisma Accelerate. A lightweight process holds a few real backends and multiplexes many clients onto them, lending a backend only for the duration of a transaction. Ten thousand clients, twenty backends.</p>
<p><strong>What it costs is session state</strong>, because the backend you get is not the one you had last time. And the way you find out is a production error that says nothing about pooling.</p>





























<table><thead><tr><th>Feature</th><th>Why it breaks under transaction pooling</th></tr></thead><tbody><tr><td>Server-side prepared statements</td><td><code>PREPARE</code> lives on one backend; your next statement may land on another. PgBouncer 1.21+ can track these — verify your version rather than assume</td></tr><tr><td>Session <code>SET</code> variables</td><td><code>SET search_path</code> or <code>SET timezone</code> applies to a backend you're about to lose. <code>SET LOCAL</code> inside a transaction is safe</td></tr><tr><td><code>LISTEN</code> / <code>NOTIFY</code></td><td>Needs a persistent session to receive on. Silently receives nothing — no error, just missing events</td></tr><tr><td>Session-level advisory locks</td><td><code>pg_advisory_lock()</code> is held by a session about to be lent elsewhere, and can never be released by its owner. Use <code>pg_advisory_xact_lock()</code></td></tr><tr><td>Temp tables, <code>WITH HOLD</code> cursors</td><td>Session-scoped objects. Gone at transaction end</td></tr></tbody></table>
<p>The failure signature is worth committing to memory, because nothing in it mentions pooling: <strong>the ORM works perfectly locally against a direct connection, and throws <code>prepared statement "s0" does not exist</code> in production.</strong> Or <code>relation "work_items" does not exist</code> halfway through a request. Or — worst of all — no error, just timezone drift producing quietly wrong date arithmetic, because a session <code>SET</code> didn't survive.</p>
<p>For Prisma specifically, the two-connection-string setup is non-negotiable, because the migration engine uses <strong>session-level advisory locks</strong> and will hang indefinitely through a transaction-mode pooler:</p>
<pre><code class="language-env">DATABASE_URL="postgresql://user:pass@pooler-host:6432/mydb?pgbouncer=true"
DIRECT_URL="postgresql://user:pass@postgres-host:5432/mydb"
</code></pre>
<pre><code class="language-prisma">datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")   // pooler, for app queries
  directUrl = env("DIRECT_URL")     // direct, for migrations
}
</code></pre>
<p>The general pattern, whatever your stack: <strong>two endpoints against one database.</strong> A transaction-mode port for application traffic, and a session-mode or direct connection for migrations, <code>LISTEN</code>-based workers, and human debugging. The cost is a second connection string to configure, get wrong once, and document.</p>
<p>One more thing worth saying plainly: session mode is the compatibility escape hatch, <strong>not</strong> a fix for exhaustion. It holds a backend for the client's entire connection, giving roughly 1:1 reuse — all of a proxy's operational cost with none of the multiplexing benefit.</p>
<hr>
<h2>Failure Mode 5: Holding a Connection Across a Call You Don't Control</h2>
<p>This one passes code review every single time.</p>
<pre><code class="language-typescript">await client.query('BEGIN');
await client.query('UPDATE orders SET status = $1 WHERE id = $2', ['charging', id]);

// Three seconds of somebody else's p99 — with a backend, a snapshot,
// and a row lock all held open, executing nothing.
const charge = await stripe.charges.create({ amount, currency: 'usd' });

await client.query('UPDATE orders SET charge_id = $1 WHERE id = $2', [charge.id, id]);
await client.query('COMMIT');
</code></pre>
<p>Run <code>L = λW</code> on it. At 50 req/s with a 3-second vendor call, you need 150 connections held to do essentially no database work. Your pool is 20.</p>
<p><strong>Your pool size is now a function of a vendor's p99.</strong> When their latency doubles, every unrelated endpoint on that instance goes down with it.</p>
<p>And <code>statement_timeout</code> will not save you — this is the genuinely useful part. No statement is running. The backend is <code>idle in transaction</code>, holding a snapshot that blocks vacuum database-wide and row locks that stall other writers, while executing nothing at all. The setting that actually fires is <code>idle_in_transaction_session_timeout</code>, which every application role should have, and which still only acts <em>after</em> the damage, aborting mid-payment.</p>
<p>The fix is structural, not configurational: commit an intent carrying an idempotency key, make the external call holding nothing, then commit the outcome in a second short transaction.</p>
<p>The cost, named honestly: one atomic operation became two, so a crash between them leaves an order stuck in <code>charging</code>. That needs a reconciliation job that finds stale intents and asks the provider what happened — which is exactly why the idempotency key is written in the <em>first</em> transaction rather than generated at call time. The trade is real: a recoverable inconsistency in exchange for not tying your pool to someone else's uptime.</p>
<hr>
<h2>The Metric Nobody Graphs</h2>
<p>Every failure mode above shares a diagnostic signature, and it's why these incidents burn hours.</p>
<p>When the pool saturates, <strong>latency accumulates before any query runs</strong> — so every tool you'd reach for measures the wrong interval:</p>
<ul>
<li><code>pg_stat_statements</code> reports execution time. Your queries look fine at 8ms.</li>
<li>Slow-query logs are silent. Nothing ran slowly.</li>
<li>Your APM's database span starts once the driver <em>already holds</em> a connection.</li>
<li>Database CPU is low, which reads as "the database is healthy" and sends the investigation into application code.</li>
</ul>
<p>Meanwhile p99 is 3 seconds, of which 2.99 were spent inside <code>pool.connect()</code> — an interval on no component's dashboard, because the app thinks it's database time and the database has never heard of the request.</p>
<p>So instrument the acquisition yourself:</p>
<pre><code class="language-typescript">async function acquire(pool: Pool) {
  const start = performance.now();
  const client = await pool.connect();
  // The number that holds your p99 during saturation. Histogram it. Alert on p99.
  metrics.histogram('db.pool.wait_ms', performance.now() - start);
  return client;
}
</code></pre>
<p>Also worth having permanently: <strong>waiting count</strong> (<code>pool.waitingCount</code>) for queue depth, <strong>in-use vs idle</strong> (<code>totalCount</code>, <code>idleCount</code>) which gives you utilisation, and <strong>acquisition timeouts per minute</strong>. Set <code>connectionTimeoutMillis</code> — unset means unbounded queueing, which is a queue with no admission control.</p>
<p>On the database side, the first query of any connection incident:</p>
<pre><code class="language-sql">SELECT state, count(*) FROM pg_stat_activity GROUP BY state ORDER BY count DESC;
</code></pre>
<p>A large <code>idle in transaction</code> count is Failure Mode 5, live in production.</p>
<p>And if you're running PgBouncer, the single most important view:</p>
<pre><code class="language-sql">SHOW POOLS;
-- cl_waiting: clients waiting for a server connection → THIS SHOULD BE 0
</code></pre>
<p><strong>Alert thresholds worth setting today:</strong> warning at 70% of <code>max_connections</code>, critical at 85% (you're roughly 90 seconds from user-visible errors), page immediately on more than 10 connections <code>idle in transaction</code>, and alert on sustained <code>cl_waiting > 0</code>.</p>
<hr>
<h2>The Fixes, and What Each One Actually Costs</h2>
<p>There is no free option. Pick the bill you'd rather pay.</p>






























<table><thead><tr><th>Fix</th><th>What it buys</th><th>What it costs</th></tr></thead><tbody><tr><td><strong>Transaction-mode pooler</strong> (PgBouncer, RDS Proxy, Supavisor)</td><td>Ten thousand clients onto twenty backends. The right answer for serverless</td><td>Session state — prepared statements, session <code>SET</code>, <code>LISTEN</code>/<code>NOTIFY</code>, session advisory locks, temp tables. Plus a network hop (~0.5–2ms) and a new component in the request path that can fail</td></tr><tr><td><strong>SQL over HTTP</strong> (Neon serverless driver, Supabase client)</td><td>Nothing persistent, so nothing to exhaust. Works in Edge Runtime, where TCP doesn't exist at all</td><td>The interactive transaction. Read-decide-write has nowhere to live, so every <code>SELECT ... FOR UPDATE</code> needs rethinking. Plus ~10–30ms per-query HTTP overhead and a vendor-specific driver</td></tr><tr><td><strong>Managed pooler + cache</strong> (Prisma Accelerate)</td><td>Pooling plus per-query TTL/SWR caching, no infrastructure to operate</td><td>Vendor dependency — your database connectivity now depends on their uptime even when your Postgres is healthy. Plus a hop, plus pricing</td></tr><tr><td><strong>Keep the pool in a long-running process</strong></td><td>Puts the pool where a pool can live; functions call it over HTTP</td><td>The thing you were trying to delete. You now operate a server with deploys, health checks, and a scaling policy — and the bottleneck relocates to <em>that</em> tier's concurrency limit</td></tr></tbody></table>
<p>Mapping that to real deployments:</p>

































<table><thead><tr><th>Infrastructure</th><th>Strategy</th></tr></thead><tbody><tr><td>Vercel / Netlify Functions</td><td>HTTP driver or managed pooler. <strong>Never</strong> a direct connection — the singleton pattern alone is insufficient</td></tr><tr><td>AWS Lambda</td><td>RDS Proxy or self-managed PgBouncer, transaction mode</td></tr><tr><td>Always-on K8s / Fly.io / Railway</td><td>Global singleton client, <code>connection_limit = floor(max_connections / max_pods) - buffer</code></td></tr><tr><td>Supabase hosted</td><td>Supavisor on port 6543 for app traffic; port 5432 for migrations only</td></tr><tr><td>Edge Runtime / Middleware</td><td>HTTP driver only — V8 isolates have no TCP sockets, so <code>pg</code>, <code>postgres.js</code>, and standard Prisma simply cannot run</td></tr><tr><td>Local dev</td><td>Direct connection, but configure <code>DIRECT_URL</code> anyway so prod parity isn't a surprise</td></tr></tbody></table>
<p>On that Edge Runtime row, the simplest advice is the best advice: <strong>don't query your database from Edge Runtime</strong> unless you're on an HTTP driver. Move database access to the Node.js runtime and reserve the edge for work that only touches KV or cache.</p>
<hr>
<h2>The Short Version</h2>
<p>A Postgres connection is a forked process with megabytes of private memory, the right to allocate <code>work_mem</code> per plan node, and a snapshot that holds back vacuum. <code>max_connections</code> is a memory budget wearing a counter's clothing.</p>
<p>The only number the database sees is <code>N instances × pool size</code>, plus workers, cron, migrations, exporters, and reserved slots. Rolling deploys briefly double the app tier, which is why these incidents track deploys rather than traffic.</p>
<p>Small pools are faster under load. <code>L = λW</code> puts required concurrency at arrival rate × hold time — usually single digits. When the pool saturates, cut <code>W</code>; don't raise <code>C</code>.</p>
<p>Transaction-mode pooling costs session state, and announces it through errors that mention prepared statements rather than pooling.</p>
<p>Never hold a connection across a call you don't control, because <code>statement_timeout</code> cannot interrupt a backend that isn't executing anything.</p>
<p>And instrument pool-wait time. It's the interval that holds your p99 during every one of these failures, and it appears on nobody's dashboard by default.</p>
<p>Connection pooling isn't a performance optimisation. It's a shared budget with a hard ceiling, spent by more consumers than anyone has counted — and in serverless, the spending is done by a concurrency number you don't control.</p>]]></content:encoded>
      <pubDate>Sun, 02 Aug 2026 08:23:44 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Serverless</category>
      <category>Performance</category>
      <category>Backend</category>
    </item>
    <item>
      <title>PostgreSQL 19 vs. 18: What Actually Changed, Feature by Feature</title>
      <link>https://insight.jatinjainsaraf.com/postgresql-19-vs-18-what-actually-changed-feature-by-feature</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/postgresql-19-vs-18-what-actually-changed-feature-by-feature</guid>
      <description>Every major PostgreSQL release gets the &quot;this changes everything&quot; headline treatment. Here&apos;s the honest version: what specifically didn&apos;t work in PG18 that PG19 now fixes, feature by feature, with the real production tradeoffs — from REPACK CONCURRENTLY to SQL/PGQ to parallel autovacuum.</description>
      <content:encoded><![CDATA[<h1>PostgreSQL 19 vs. 18: What Actually Changed, Feature by Feature</h1>
<p>Every major PostgreSQL release gets the same LinkedIn headline treatment: "this changes everything." Most of the time it doesn't, it's a dozen genuine improvements wrapped in the language of a paradigm shift. PostgreSQL 19 is a real release with real substance, but the honest way to evaluate it isn't the headline feature, it's asking what specifically didn't work in PG18 that now does, and what the actual cost of adopting it is.</p>
<hr>
<h3>The Shape of the Two Releases</h3>
<p>PostgreSQL 18 was primarily an <strong>I/O and observability</strong> release: the Asynchronous I/O subsystem rewired how the engine reads from disk, <code>pg_stat_io</code> got a near-total overhaul, and B-Tree skip scans fixed a decade-old multicolumn-index limitation. PostgreSQL 19 is primarily a <strong>DDL/DML ergonomics and operational-maintenance</strong> release: the headline feature (SQL/PGQ) is a new query language surface, but the features that will actually change your on-call life are <code>REPACK CONCURRENTLY</code>, native partition reshaping, and parallel autovacuum.</p>
<p>That distinction matters because it tells you where to look for value. If PG18 already fixed your I/O-bound scan performance, PG19 isn't going to double it again, it's going to fix the maintenance operations that PG18 left untouched.</p>
<h3>1. SQL/PGQ: Property Graph Queries</h3>
<p><strong>What it is:</strong> PostgreSQL 19 adds SQL/PGQ, the SQL:2023 standard for querying graph-shaped data. You define a <strong>property graph</strong> as a read-only view over existing relational tables (a node table, an edge table), then query it with <code>MATCH</code> and Neo4j-style arrow syntax: <code>MATCH (a:Employee)-[:MANAGES]->(b:Employee)</code>. Internally, PostgreSQL rewrites the arrow syntax into ordinary joins against your existing tables before the planner ever runs, so it inherits your existing indexes and your existing row-level security automatically.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> In PG18, and every version before it, modeling a graph relationship (a dependency tree, an org chart, an authorization graph, "who reports to whom") meant writing a recursive CTE, <code>WITH RECURSIVE</code>, walking a self-referencing table one level at a time. Recursive CTEs work, but they're procedural: you write the recursion, you write the termination condition, and the planner frequently struggles to estimate the cost of a deep or unbounded traversal, which is exactly the kind of query that "chokes the planner" that gets complained about on social media. There was no declarative way to say "find all paths matching this pattern" — you had to hand-build the loop yourself in SQL.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>No secondary graph database, no sync pipeline. If your graph queries are shallow-to-medium depth over data that's already relational, you get graph-shaped querying without standing up Neo4j and building an ETL job to keep it current.</li>
<li>Declarative pattern matching gives the planner a clearer picture of intent than a hand-written recursive CTE, which can lead to better plans for the same logical query.</li>
<li>Inherits your existing security model for free, since a property graph is "just a view."</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>It's still running joins under the hood. You do <strong>not</strong> get index-free adjacency, the property that makes a native graph database like Neo4j fast for deep, multi-hop traversal (5+ hops over millions of edges). If your actual workload is that kind of traversal, SQL/PGQ won't rescue you from needing a dedicated graph store.</li>
<li>New syntax surface means new things to learn and new things the planner can misjudge; it's not yet battle-tested the way recursive CTEs are, having existed for two decades.</li>
<li>The realistic audience for this feature is "developers who currently reach for a recursive CTE for shallow hierarchical queries," not "teams running production graph analytics at Neo4j scale." Know which one you are before switching.</li>
</ul>
<h3>2. <code>REPACK</code>: Unifying and Unlocking <code>VACUUM FULL</code> / <code>CLUSTER</code></h3>
<p><strong>What it is:</strong> PostgreSQL 19 introduces a single <code>REPACK</code> command that replaces both <code>VACUUM FULL</code> (rewrite the table to reclaim bloat) and <code>CLUSTER</code> (rewrite the table in index order). The critical addition is a <code>CONCURRENTLY</code> option — <code>REPACK ... CONCURRENTLY</code> — that rebuilds the table <strong>without</strong> taking the <code>ACCESS EXCLUSIVE</code> lock that made both of the old commands unusable on a live, high-traffic table.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> In PG18, if a table had accumulated enough bloat (dead row versions from updates/deletes that <code>VACUUM</code> alone couldn't reclaim) that you needed to physically shrink it, your only in-core option was <code>VACUUM FULL</code>, which locks the table exclusively for the entire rewrite. On a table serving live traffic, that's not a maintenance task, it's a scheduled outage. The workaround was the third-party <code>pg_repack</code> extension, which achieves a similar result without full locking by building a shadow copy and swapping it in, but that means installing and trusting an external extension for something this fundamental.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>This is the single biggest operational upgrade in the release for anyone who has had to fight table bloat on a production system: the capability that <code>pg_repack</code> (the extension) existed specifically to provide is now in core, with <code>CONCURRENTLY</code>.</li>
<li>One command name instead of two (<code>VACUUM FULL</code> and <code>CLUSTER</code> remain for backward compatibility, but <code>REPACK</code> is now the unified entry point).</li>
<li>Removes a dependency on a third-party extension for a core maintenance operation.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><code>CONCURRENTLY</code> avoiding the exclusive lock doesn't mean it's free, it still consumes I/O and CPU rewriting the table, and the new <code>max_repack_replication_slots</code> variable exists because concurrent repack has its own resource considerations to tune.</li>
<li>If your team already has <code>pg_repack</code> (the extension) working reliably, migrating to core <code>REPACK CONCURRENTLY</code> is a "nice to have simplify," not an urgent fix, don't rip out something that already works without testing the native replacement first.</li>
</ul>
<h3>3. Native Partition Merge/Split</h3>
<p><strong>What it is:</strong> <code>ALTER TABLE ... MERGE PARTITIONS</code> and <code>ALTER TABLE ... SPLIT PARTITIONS</code>, letting you reshape an existing partitioned table's partition boundaries natively.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> PG18 invested heavily in partition <em>query performance</em>, more efficient planning over many partitions, better partitionwise joins, reduced memory use for partition pruning, but did nothing for partition <em>maintenance</em>. If a monthly partition grew too large and you wanted to split it into two, or several small partitions had accumulated and you wanted to merge them, there was no native command. You did it by hand: create new partition(s), migrate the relevant rows with <code>INSERT ... SELECT</code>, detach and drop the old partition, all while carefully managing locks and application downtime windows.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Turns a multi-step, error-prone, DBA-scripted operation into a single DDL statement.</li>
<li>Makes it realistic to actually right-size partitions over time as data volume and access patterns change, rather than living with whatever partition scheme you picked at design time.</li>
<li>Complements PG18's partition query-planning work: PG18 made partitioned tables fast to <em>query</em>, PG19 makes them practical to <em>maintain</em>.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Any partition reshape on a large table still means physically moving rows, this is not a free metadata-only operation, plan for I/O and lock impact accordingly.</li>
<li>Doesn't retroactively fix a bad initial partitioning key choice, it makes <em>boundary</em> changes easier, not a full re-partitioning strategy change.</li>
</ul>
<h3>4. <code>GROUP BY ALL</code></h3>
<p><strong>What it is:</strong> New <code>SELECT</code> syntax, <code>GROUP BY ALL</code>, which automatically groups by every column in the <code>SELECT</code> list that isn't an aggregate or window function, no manual enumeration required.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> PG18 addressed a related but distinct problem at the planner level: it learned to <em>ignore</em> <code>GROUP BY</code> columns that were functionally dependent on other grouped columns (for example, if you group by a table's unique primary key, other same-table columns don't logically need to be listed, and PG18's planner recognized this and dropped them from the actual grouping operation for efficiency). That's an internal cost optimization, it didn't change what you had to <em>type</em>. In PG18 you still hand-wrote every column in the <code>GROUP BY</code> clause, no matter how wide the <code>SELECT</code> list.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Removes the tedious, error-prone task of keeping a long <code>GROUP BY</code> list in sync with the <code>SELECT</code> list, adding a column to one and forgetting the other is a classic source of "column must appear in GROUP BY" errors.</li>
<li>Particularly valuable for wide analytical queries with 10+ grouping dimensions.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Implicit grouping means it's slightly easier to accidentally group by more (or fewer) columns than you intended if the <code>SELECT</code> list changes later, explicit lists are more self-documenting for complex queries. Use with intention, not as a default habit for every query.</li>
</ul>
<h3>5. <code>FOR PORTION OF</code>: Temporal UPDATE/DELETE</h3>
<p><strong>What it is:</strong> New clause for <code>UPDATE</code> and <code>DELETE</code>, <code>FOR PORTION OF &#x3C;period> FROM &#x3C;start> TO &#x3C;end></code>, that lets you modify or delete just a sub-range of a temporal (range-based) row, automatically splitting the surrounding range as needed.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> PG18 introduced the <em>constraint</em> half of temporal tables: <code>WITHOUT OVERLAPS</code> for <code>PRIMARY KEY</code>/<code>UNIQUE</code> and <code>PERIOD</code> for foreign keys, letting you enforce that time ranges in a table never overlap. But it gave you no corresponding <em>operation</em> half. If you had a row valid from Jan–Dec and needed to change just the March–June portion, you had to manually delete the original row and insert two (or three) new rows representing the split ranges yourself, exactly the kind of fiddly, off-by-one-prone logic a database feature should be doing for you.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Closes the gap PG18 left half-finished: PG18 gave you guardrails (non-overlapping constraints), PG19 gives you the verbs (safely editing a slice of history without hand-rolling the split).</li>
<li>Meaningful for any system tracking effective-dated data, pricing history, contract terms, HR assignment periods, insurance coverage windows.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Temporal tables remain a niche feature relative to PostgreSQL's overall audience, most applications don't model data this way, and adopting <code>WITHOUT OVERLAPS</code> + <code>FOR PORTION OF</code> is a genuine schema-design commitment, not a drop-in swap.</li>
</ul>
<h3>6. <code>INSERT ... ON CONFLICT DO SELECT ... RETURNING</code></h3>
<p><strong>What it is:</strong> Extends <code>ON CONFLICT</code> beyond <code>DO NOTHING</code>/<code>DO UPDATE</code> with a new <code>DO SELECT ... RETURNING</code> option, returning (and optionally locking, via <code>FOR UPDATE</code>/<code>FOR SHARE</code>) the row that caused the conflict, without modifying it.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> PG18 made real progress on returning row state, adding <code>OLD</code>/<code>NEW</code> alias support to <code>RETURNING</code> across <code>INSERT</code>/<code>UPDATE</code>/<code>DELETE</code>/<code>MERGE</code>, so you could see before-and-after values in a single statement. But <code>ON CONFLICT</code> itself was unchanged: still only <code>DO NOTHING</code> (silently skip, tell you nothing about the existing row) or <code>DO UPDATE</code> (you must actually modify something to get a <code>RETURNING</code> result). The common "get-or-create" pattern, insert a row if it doesn't exist, otherwise just give me the existing one, had no clean native expression; teams worked around it with a no-op <code>DO UPDATE SET col = col</code> just to trigger a <code>RETURNING</code> clause.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Directly solves get-or-create without a no-op write, which matters for both clarity and for avoiding unnecessary row versions from a fake update (fewer wasted row versions means less bloat, which loops back to why <code>REPACK CONCURRENTLY</code> matters less often).</li>
<li>Optional row locking (<code>FOR UPDATE</code>/<code>FOR SHARE</code>) on the conflicting row means you can safely read-then-act on it within the same statement, useful for concurrent upsert-adjacent logic.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Another <code>ON CONFLICT</code> branch to learn and to get right in mixed application code that already juggles <code>DO NOTHING</code> and <code>DO UPDATE</code> logic; worth auditing existing upsert helper functions to see where this actually simplifies things versus where it's unnecessary.</li>
</ul>
<h3>7. TOAST Default Compression: <code>pglz</code> → <code>lz4</code></h3>
<p><strong>What it is:</strong> The default compression algorithm for TOASTed (large, out-of-line) values changes from <code>pglz</code> to <code>lz4</code>.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> <code>lz4</code> TOAST compression already existed as an option in PG18 (and earlier), you could set <code>default_toast_compression = lz4</code> or specify it per-column. But almost nobody did, because defaults are what most schemas actually run with. PG18 shipped with <code>pglz</code> as the out-of-the-box default; PG19 flips that default.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>A genuinely free win for anyone storing large JSONB, text, or bytea values who never manually tuned this setting, <code>lz4</code> compresses and decompresses meaningfully faster than <code>pglz</code> at a comparable ratio.</li>
<li>Zero migration effort for new databases; the new default just applies.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Existing tables don't retroactively recompress, this only affects newly TOASTed values going forward (or values rewritten via <code>REPACK</code>/<code>VACUUM FULL</code>), so an existing large database won't see the benefit until data is naturally rewritten or explicitly reprocessed.</li>
</ul>
<h3>8. Parallel Autovacuum Workers</h3>
<p><strong>What it is:</strong> A single autovacuum job on one table can now use multiple parallel workers, controlled globally by <code>autovacuum_max_parallel_workers</code> and per-table by the <code>autovacuum_parallel_workers</code> storage parameter. PG19 also adds a scoring system (<code>autovacuum_vacuum_score_weight</code>, <code>autovacuum_freeze_score_weight</code>, and related variables) to decide which tables get processed first, replacing a simpler threshold-only heuristic.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> PG18 improved autovacuum's <em>behavior</em> significantly: "eager freezing" let normal vacuums freeze all-visible pages proactively (reducing the cost of a later full freeze), <code>autovacuum_worker_slots</code> let you raise the effective worker cap at runtime without a restart, and <code>autovacuum_vacuum_max_threshold</code> let you set a fixed dead-tuple trigger point instead of relying purely on percentages. What PG18 didn't change: <strong>each individual vacuum job on a given table still ran as one single worker process</strong>. On a genuinely huge, high-churn table, that one worker was the throughput ceiling, no matter how many total autovacuum worker slots your cluster had available.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Directly attacks the specific case that bites teams running very large, high-write tables: a vacuum job that used to take hours on one worker can now split the work across several, on the same table, at the same time.</li>
<li>The new scoring system for processing order is a real improvement over pure threshold checks, better prioritizing which of many candidate tables actually needs attention most urgently.</li>
<li>Complements, rather than duplicates, PG18's improvements, PG18 made each vacuum pass smarter and more proactive; PG19 makes the biggest passes faster by parallelizing them.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>More parallel workers means more concurrent I/O and CPU contention during vacuum; on a system already tight on resources, this needs the same careful tuning any parallelism feature does, it's not automatically a net win without headroom to spend.</li>
<li>Per-table <code>autovacuum_parallel_workers</code> is one more tuning knob DBAs now need to understand and set deliberately for their biggest tables, rather than relying entirely on cluster-wide defaults.</li>
</ul>
<h3>9. Logical Replication: Native Sequence Synchronization</h3>
<p><strong>What it is:</strong> Sequences can now be included in logical replication. <code>CREATE</code>/<code>ALTER PUBLICATION ... ALL SEQUENCES</code> publishes all sequences, and <code>ALTER SUBSCRIPTION ... REFRESH SEQUENCES</code> syncs sequence <em>values</em> (not just existence) on the subscriber to match the publisher. <code>pg_get_sequence_data()</code> lets you inspect sync state directly.</p>
<p><strong>Comparison (PG18 vs. PG19):</strong> PG18 made solid logical replication improvements, generated column values could finally be replicated, and the default streaming mode for new subscriptions switched from <code>off</code> to <code>parallel</code> for better apply performance. But sequences were entirely untouched by logical replication in PG18 and every prior version: a subscriber's sequences had no relationship to the publisher's. If you promoted a logically-replicated subscriber to primary during a failover, its sequences would still be wherever they last were on that subscriber, not caught up to the publisher's actual <code>nextval()</code> position, unless you manually reset them yourself before cutting traffic over.</p>
<p><strong>Pros:</strong></p>
<ul>
<li>Removes a genuinely dangerous, well-known logical-replication gap: without this, a poorly-timed failover onto a logically-replicated subscriber can hand out primary-key values that collide with rows the old primary already committed, a duplicate-ID bug hiding specifically in your disaster-recovery path, the worst possible place for a bug to hide since it only shows up when you're already in an incident.</li>
<li><code>pg_get_sequence_data()</code> gives visibility into sync state that simply didn't exist before, useful for confirming a subscriber is actually safe to promote.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>This is a correctness fix for a previously-silent gap, not a performance feature, teams need to actively adopt <code>ALL SEQUENCES</code> and <code>REFRESH SEQUENCES</code> in their subscription setup; it isn't retroactively applied to existing subscriptions without action.</li>
</ul>
<h3>Which of These Actually Change How You Operate Postgres</h3>
<p>Ranking by realistic production impact, not headline appeal:</p>
<ol>
<li><strong><code>REPACK CONCURRENTLY</code></strong> — removes a hard operational constraint (mandatory downtime for bloat reclaim) that has existed since the beginning of Postgres.</li>
<li><strong>Parallel autovacuum</strong> — directly extends throughput on the exact tables where autovacuum has historically fallen behind.</li>
<li><strong>Logical replication sequence sync</strong> — closes a real correctness gap sitting specifically in failover paths.</li>
<li><strong>Partition merge/split</strong> — makes partition schemes maintainable as data grows, rather than fixed at design time.</li>
<li><strong>SQL/PGQ</strong> — genuinely useful for a specific class of query (shallow hierarchical/relationship modeling), not a universal graph-database replacement, know which camp you're in before treating it as a headline reason to upgrade.</li>
<li>Everything else (<code>GROUP BY ALL</code>, <code>FOR PORTION OF</code>, <code>ON CONFLICT DO SELECT</code>, TOAST <code>lz4</code> default) — real, welcome ergonomics and default-quality improvements, but incremental rather than architectural.</li>
</ol>
<h3>Where This Fits</h3>
<p>If you're running the <a href="https://academy.jatinjainsaraf.com/postgresql-in-depth">PostgreSQL In-Depth course</a>, the maintenance-and-bloat modules built around <code>pg_repack</code> and manual <code>VACUUM FULL</code> tradeoffs are the direct prerequisite for understanding why <code>REPACK CONCURRENTLY</code> matters as much as it does, and the partitioning phase is the natural place to slot in the new merge/split DDL. For the deep architectural picture PG19 builds on top of (process model, MVCC, the planner, WAL), see <a href="https://insight.jatinjainsaraf.com/the-postgresql-elephant-in-the-room-a-deep-dive-into-the-architecture-that-powers-giants">"The PostgreSQL Elephant in the Room."</a></p>
<p>PostgreSQL 19 isn't a rewrite of what Postgres is, it's the same server process, managing files intelligently, that <a href="https://insight.jatinjainsaraf.com/what-the-postgresql-server-is-actually-doing">"What the PostgreSQL Server Is Actually Doing"</a> describes, just with more of the maintenance and ergonomic rough edges sanded down. That's not a smaller story than "Postgres killed the recursive CTE", it's a more honest one.</p>
<p>#PostgreSQL #Database #Backend #SoftwareEngineering #DatabaseInternals #SQL #DevOps</p>]]></content:encoded>
      <pubDate>Mon, 27 Jul 2026 15:23:36 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Backend</category>
      <category>Software Engineering</category>
    </item>
    <item>
      <title>Idempotency Keys: How to Make Retries Safe in Distributed Systems</title>
      <link>https://insight.jatinjainsaraf.com/idempotency-keys-how-to-make-retries-safe-in-distributed-systems</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/idempotency-keys-how-to-make-retries-safe-in-distributed-systems</guid>
      <description>A network blip retries a payment request. Without an idempotency key, the customer is charged twice. Here is why retries are unavoidable, how idempotency keys make them safe, and when a blockchain indexer replaying the same block twice must not double-count it.</description>
      <content:encoded><![CDATA[<h1>Idempotency Keys: How to Make Retries Safe in Distributed Systems</h1>
<p>A customer clicks "Pay Now" once. The request reaches your payment service, the charge succeeds, but the response times out on the way back. The client, seeing no answer, retries. Somewhere in your logs there are now two successful charges for one click. Nobody wrote a bug. The network just did what networks do.</p>
<hr>
<h3>Retries Are Not Optional, They're the Default</h3>
<p>In a single process, a function call either returns or the whole program crashes with it. In a distributed system, that guarantee disappears. A request can fail before it reaches the server, after the server processes it but before the response comes back, or anywhere in between, and from the caller's side, all three failures look identical: silence, or a timeout.</p>
<p>Given that, a caller has exactly two honest choices: give up, or retry. Almost every serious system chooses to retry, because giving up on a payment, an order, or a blockchain transaction just because a router hiccupped is worse than the alternative. The problem is that "the alternative" retrying turns a network problem into a correctness problem, unless the operation you're retrying can tolerate being run more than once.</p>
<p>That property has a name: <strong>idempotency</strong>. An operation is idempotent if running it once and running it five times leave the system in the same state. <code>SET x = 5</code> is idempotent — running it twice still leaves <code>x</code> at 5. <code>x = x + 5</code> is not, run it twice and you've added 10. Retrying a network call is safe by default only when the operation behind it is naturally idempotent. Charging a card, placing an order, sending an email — none of these are. That's the gap idempotency keys exist to close.</p>
<h3>How an Idempotency Key Actually Works</h3>
<p>The mechanism is simpler than the name suggests. The client generates a unique key, typically a UUID, once, at the moment the user takes the action, before any request is sent. That key is attached to the request, usually as a header (<code>Idempotency-Key: 8f14e...</code>). Every retry of that same logical action, the same click, the same order, reuses the exact same key.</p>
<p>On the server, before doing any real work, the first thing that happens is a lookup: has this key been seen before?</p>
<ul>
<li><strong>Not seen before</strong> → this is genuinely a new request. Process it (charge the card, place the order), store the key alongside the result, and return.</li>
<li><strong>Seen before, and finished</strong> → this is a retry of something already done. Skip the actual work entirely and return the <em>stored result</em> from the first attempt. The client gets the same success response it would have gotten the first time, and no charge happens twice.</li>
<li><strong>Seen before, and still in progress</strong> → a retry arrived while the original request is still being processed (a slow response, not a failed one). The correct move is to make the retry wait or reject it, never to run the operation again concurrently.</li>
</ul>
<p>That storage of "key → result" is the entire trick. It turns "run this action" into "run this action, but only the first time you're asked, and remember what happened so every later ask gets the same answer." The database row or cache entry holding that key is doing the same job as the WAL in PostgreSQL, giving you a durable record of intent so a retry, a crash, or a redelivery doesn't have to guess what already happened.</p>
<h3>Why This Matters in Production, Not Just in Theory</h3>
<p>This isn't a hypothetical edge case; it's the default behavior of almost every reliability mechanism you already rely on:</p>
<ul>
<li><strong>Payment gateways retry.</strong> Stripe, for instance, builds idempotency keys into its API specifically because a client-side timeout on a successful charge is common enough to have a name in their docs.</li>
<li><strong>Message queues retry.</strong> SQS, Kafka consumers, and most queue systems offer <em>at-least-once</em> delivery, not <em>exactly-once</em>, because exactly-once delivery across a network is provably expensive to guarantee. "At least once, so dedupe on your end" is the honest tradeoff, and idempotency keys are "your end." This is exactly why background job systems like BullMQ build <a href="https://academy.jatinjainsaraf.com/nodejs-in-depth/background-jobs-bullmq">retries with exponential backoff and dead letter queues</a> into the framework itself, rather than leaving it to each job handler to reinvent.</li>
<li><strong>Mobile clients retry.</strong> A user on a bad connection taps "Submit" once, but the app, seeing no response after a few seconds, quietly retries the request behind the scenes. Without an idempotency key, that one tap becomes two orders.</li>
<li><strong>Load balancers and proxies retry.</strong> Some infrastructure automatically retries a request that failed to connect, before your application code ever sees it happen once, let alone twice.</li>
<li><strong>Webhooks retry, by design.</strong> Stripe, GitHub, and most webhook senders will redeliver an event if your endpoint doesn't return a fast 2xx response, on the assumption that a timeout means you never got it. <a href="https://academy.jatinjainsaraf.com/nodejs-in-depth/external-services-caching">Idempotent webhook processing</a> — checking the event ID before acting on it — is the only thing standing between that redelivery and a duplicate side effect on your end.</li>
</ul>
<p>Without an idempotency key sitting between "the network is unreliable" and "the operation isn't naturally repeatable," every one of these standard, unremarkable mechanisms becomes a live double-charge, double-order, or double-send bug waiting for the right timing to trigger it.</p>
<h3>Example: Why a Blockchain Indexer Cannot Survive Without This</h3>
<p>Nowhere is this more visible, or more unforgiving, than in a blockchain indexer, and it's worth walking through concretely.</p>
<p>An indexer's whole job is to watch a chain, pull each new block, extract the events or transfers inside it, and write them into your own database so your application can query them fast. That sounds like a simple one-pass pipeline: get block, process block, move on. In practice it never runs exactly once per block, for reasons entirely outside your control:</p>
<ul>
<li><strong>Reorgs.</strong> The chain itself can rewind and replay a range of blocks when a fork resolves. Your indexer will see block 18,402,001 more than once, as two different, competing versions of "reality."</li>
<li><strong>Crash recovery.</strong> If your indexer process dies mid-batch, on restart it doesn't know precisely which of the last few blocks were fully written versus half-written, so the safe move is to reprocess a small overlapping window, not trust a fragile "last processed block" pointer to be exactly right.</li>
<li><strong>RPC retries.</strong> The call to fetch a block from a node can itself time out and get retried, occasionally landing you the same block payload twice from two separate requests racing each other.</li>
</ul>
<p>Now picture what happens without idempotency built in: a transfer event says "500 USDC moved from wallet A to wallet B." If that event gets processed twice, because of a reorg replay or a crash-recovery reprocess, and your indexing logic does <code>balance += 500</code> each time it sees the event, wallet B's balance is now wrong by 500 USDC in your database, permanently, until someone notices the number doesn't match the chain and re-runs a full backfill. On a system indexing tens of millions of events, that's not a rare accident, it's a near-certainty over enough uptime.</p>
<p>The fix is the exact same pattern as the payment example, just with a different key. Instead of a client-generated UUID, the natural idempotency key is something already unique to the data itself: <code>(transaction_hash, log_index)</code> for an EVM chain, for instance, uniquely identifies one specific event, no matter how many times it's delivered to you. The write becomes an upsert keyed on that pair, not a blind <code>balance += amount</code>. Seen this <code>(tx_hash, log_index)</code> before? Skip it, or overwrite with the identical result, never add on top of it again. Reorgs, crash-recovery reprocessing, and duplicate RPC responses all become harmless, because the operation of "record this event" is now idempotent, exactly the same property a payment API gets from a client-supplied key.</p>
<h3>Where to Put the Key</h3>
<p>Idempotency keys work at whatever layer needs the guarantee, and the source of the key changes depending on who's best positioned to know "this is the same logical request":</p>
<ul>
<li><strong>Client-generated</strong> (payments, order placement, form submissions): the client mints a UUID once, before the first attempt, and resends the identical key on every retry of that same user action.</li>
<li><strong>Data-derived</strong> (blockchain events, webhook deliveries, message queue consumers): the key comes from something already unique in the payload itself, a transaction hash, a webhook delivery ID, a message ID, so you never have to coordinate key generation between sender and receiver at all.</li>
</ul>
<p>Either way, the underlying requirement is the same: the key must be stored durably, checked before any side effect runs, and scoped to an appropriate time window (payment idempotency keys are typically only guaranteed for 24 hours; a blockchain indexer's <code>(tx_hash, log_index)</code> uniqueness is effectively permanent).</p>
<h3>Where This Fits in the Full Course</h3>
<p>Idempotent webhook processing and BullMQ retry/backoff patterns are both covered hands-on in the <a href="https://academy.jatinjainsaraf.com/nodejs-in-depth">Node.js In-Depth course</a>, in the Practitioner phase — module P-7 ("Connecting External Services and Caching") and module P-12 ("Background Jobs and Task Queues with BullMQ"), respectively.</p>
<h3>The Question Worth Asking Yourself</h3>
<p>Next time you write a piece of code that has a side effect, charges something, sends something, increments something, and sits behind a network call, ask one question before you ship it: <em>if this exact request arrived twice, on purpose or by accident, would the result still be correct?</em></p>
<p>If the honest answer is no, that's not a rare failure mode you're accepting, it's a bug you've already written. The network will find it for you eventually. It's better to find it first.</p>
<p>#DistributedSystems #SoftwareEngineering #Backend #SystemDesign #Blockchain #APIDesign</p>]]></content:encoded>
      <pubDate>Fri, 24 Jul 2026 20:34:05 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Backend</category>
      <category>System Design</category>
      <category>Distributed Systems</category>
      <category>Architecture</category>
    </item>
    <item>
      <title>How the LRU Cache Actually Works and Why Redis, Browsers, and Postgres All Approximate It</title>
      <link>https://insight.jatinjainsaraf.com/how-the-lru-cache-actually-works-and-why-redis-browsers-and-postgres-all-approximate-it</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/how-the-lru-cache-actually-works-and-why-redis-browsers-and-postgres-all-approximate-it</guid>
      <description>Every fast system you use eventually has to decide what to throw away. The Least Recently Used policy is the answer nearly all of them land on, here&apos;s the two-structure trick that makes it instant, and why Redis, your browser, and Postgres all quietly cheat on the textbook version at scale.</description>
      <content:encoded><![CDATA[<p>You've felt this before: you restart a service, or open your laptop after the weekend, and everything is sluggish for the first few minutes. Every page load feels like it's dragging. Then, without you doing anything, it speeds back up. Nothing changed in your code. What changed is that the cache went cold, and it just warmed back up.</p>
<p>Almost every system that makes this speed-up possible Redis, your browser, your database, even a function that "remembers" its last few results is solving the exact same problem underneath: it has limited room, so when that room runs out, it has to decide what to throw away first. The answer nearly all of them land on is the same one: throw away whatever hasn't been touched in the longest time. That policy has a name Least Recently Used, or LRU and the way it's actually built is one of the more satisfying "two weak pieces make one strong piece" stories in software.</p>
<h2>The problem neither obvious answer solves</h2>
<p>Say you want to build this yourself: a fixed-size cache that instantly tells you if something is stored, and instantly evicts the "stalest" entry the moment you're full.</p>
<p>Your first instinct might be a plain lookup table a dictionary, a hash map, whatever your language calls it. That gives you instant answers to "is this here?" But a lookup table has no concept of time. It doesn't know that key A was touched ten seconds ago and key B was touched ten minutes ago. To find the stalest entry, you'd have to check everything slow, and it gets slower as the cache grows.</p>
<p>So your second instinct might be a simple ordered list keep every entry in a line, most recently used at the front. That solves the ordering problem. But now finding a specific entry means walking the line from the front until you happen to find it. And worse, once you find it, moving it to the front means pulling it out of the middle of the line which, in a plain list, means shifting everything around it.</p>
<p>Neither piece alone works. One gives you instant lookup with no memory of time. The other gives you a sense of time with no instant lookup.</p>
<h2>The fix: don't choose, combine</h2>
<p>The actual solution used almost everywhere is to stop treating "look something up" and "track its age" as the same job, and instead let one structure handle each, wired together.</p>
<p>Picture a line of people, ordered by who was served most recently front of the line is "just here," back of the line is "hasn't been seen in ages." That's your time-ordering structure, and because it's a <em>doubly linked</em> line (each person knows who's directly in front of and behind them), pulling anyone out of the middle and moving them to the front is instant you're just relinking a few neighbors, not shuffling the whole line.</p>
<p>Now add a cashier holding a notebook that maps every person's name directly to their exact spot in that line. You don't scan the line to find someone you check the notebook, and it points you straight at them. That notebook is your lookup table, except its entries don't hold the person's information directly they hold a pointer to where that person is standing.</p>
<p>Put those two together and something interesting happens: a lookup stops being "search the line" and becomes "check the notebook, then relink two spots in the line." Both steps are instant, regardless of how many people are in line. The lookup table gives you the address; the line gives you a cheap way to reorder once you're there. Neither piece is doing the other's job they're doing their own job, wired to the same underlying entries.</p>
<p>Eviction is the same trick pointed at the back of the line instead of the front: whoever is standing at the very back is, by definition, the stalest entry so when the cache is full, that's who gets dropped, and the notebook forgets their name too.</p>
<h2>Why real systems don't build this exactly</h2>
<p>Here's the part worth sitting with: at genuinely large scale, most real caching systems <em>don't</em> implement pure LRU, because keeping a perfectly ordered "line" updated on literally every single read becomes its own overhead once you're handling millions of operations a second.</p>
<p>Redis's LRU eviction, for instance, doesn't maintain one global perfectly-ordered line at all it samples a handful of random keys and evicts whichever of <em>those</em> looks stalest, repeated as needed. It's an approximation, and a good one, because it gets 90% of the benefit of true LRU for a fraction of the bookkeeping cost.</p>
<p>Your browser's HTTP cache and CDN edge caches lean on the same core idea evict whatever's gone longest untouched once storage fills up to decide what to drop when they hit their limits, which is exactly why re-visiting a page you loaded five minutes ago feels instant, but a page from three weeks ago fetches fresh.</p>
<p>Databases play the same game one layer down. A database's in-memory buffer pool the pages of the table currently held in RAM instead of read from disk often uses something called clock-sweep instead of textbook LRU: cheaper to maintain, same underlying goal of keeping "hot" pages in memory and letting cold ones get evicted first.</p>
<p>And plenty of ordinary application code hits this without ever calling it LRU by name "cache the last 500 computed results, and once that limit is hit, drop whatever hasn't been asked for in a while" is the identical problem, just at a size where the textbook version runs perfectly well with no approximation needed.</p>
<h2>The actual takeaway</h2>
<p>The mechanism is almost deceptively small once you see it: pair a lookup table with an ordered line, and let the table's entries point directly into the line instead of duplicating what's in it. That's the whole trick. What's genuinely interesting is how far that one idea travels from a toy interview problem, to the reason your database feels fast after warming up, to the reason revisiting a webpage is instant and revisiting an old one isn't. You've probably relied on a system built this way today without ever knowing it had a name.</p>]]></content:encoded>
      <pubDate>Tue, 21 Jul 2026 19:19:44 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>DSA</category>
      <category>System Design</category>
      <category>Caching</category>
      <category>Redis</category>
      <category>Data Structures</category>
    </item>
    <item>
      <title>Why a Single ALTER TABLE Can Take Down Your Whole Database</title>
      <link>https://insight.jatinjainsaraf.com/why-a-single-alter-table-can-take-down-your-whole-database</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/why-a-single-alter-table-can-take-down-your-whole-database</guid>
      <description>A routine ALTER TABLE queues behind one slow query — and every SELECT that arrives after it queues too, even though they&apos;d normally run just fine together. This is the PostgreSQL locking mechanic behind some of the ugliest production outages, and the two settings that prevent it.</description>
      <content:encoded><![CDATA[<h1>Why a Single ALTER TABLE Can Take Down Your Whole Database</h1>
<p>A team runs a routine migration: <code>ALTER TABLE transactions ADD COLUMN region TEXT</code>. Nothing dramatic on paper. Twelve minutes later the connection pool is full, every request from every user is failing, and on-call is paged. The migration itself was never the problem. A lock queue was.</p>
<hr>
<h3>A Lock Isn't a Wall, It's a Queue Ticket</h3>
<p>The word "lock" makes people picture a barrier: something is locked, everything else waits outside. That's not quite how PostgreSQL locking works, and the difference is exactly what causes outages like this one.</p>
<p>A better picture is a queue ticket. Every statement that touches a table takes a ticket. Some tickets are compatible with each other, holders can be served side by side, no problem. Other tickets are not compatible, and a ticket holder has to wait for the incompatible one ahead of it to finish before it can be served.</p>
<p>Two plain <code>SELECT</code> statements are always compatible. They take the weakest kind of ticket there is, and a hundred of them can run at once without ever noticing each other. The trouble starts when someone joins the line holding the strongest possible ticket.</p>
<h3>The Strongest Ticket Blocks Everyone, Even the Compatible Ones</h3>
<p>PostgreSQL has a handful of table-level lock strengths, but only one really matters for this story. It's called <code>ACCESS EXCLUSIVE</code>, and it's acquired by <code>ALTER TABLE</code>, <code>DROP TABLE</code>, and <code>TRUNCATE</code>. It conflicts with every other lock in the system, including a plain read.</p>
<p>Here's the part that catches people off guard: PostgreSQL mostly serves lock requests in the order they arrive. So if an <code>ALTER TABLE</code> shows up and has to wait for one long-running <code>SELECT</code> to finish, it doesn't just wait quietly off to the side. It gets in line. And every <code>SELECT</code> that shows up after it also has to get in line, behind the <code>ALTER TABLE</code>, even though those new <code>SELECT</code> statements would have been perfectly happy running alongside the original one.</p>
<p>One slow reader plus one waiting migration is enough to freeze every future read on that table, until the slow reader finally lets go.</p>
<h3>How Twelve Minutes Becomes an Outage</h3>
<p>This is close to how it actually plays out in production. An analytics query kicks off against a busy table and, because nobody set a timeout on it, keeps running for twelve minutes. A few minutes in, someone ships a routine schema migration on that same table. The migration asks for its <code>ACCESS EXCLUSIVE</code> ticket and starts waiting on the analytics query.</p>
<p>From that moment, every new request the application sends to that table queues up behind the migration. Within half a minute, hundreds of connections are stuck waiting on a lock, not on any actual work. The connection pool, which was never designed to hold hundreds of idle-but-blocked connections, fills up completely. New requests can't even get a connection to wait with. The site goes down, and the root cause line in the postmortem is almost funny in how small it is: one long read, one migration with no timeout.</p>
<h3>The Fix Is Two Settings, Not a Rewrite</h3>
<p>The defense here isn't clever code, it's two timeouts that should be set before any schema change ever touches a live table.</p>
<p>The first tells the migration itself to give up quickly if it can't get its lock in a few seconds, rather than parking itself at the front of a queue that keeps growing. The second is a ceiling on how long any query is allowed to run at all, so a stray analytics query can never hold a lock for twelve minutes in the first place. Neither setting is exotic. Both are usually just missing.</p>
<p>Set correctly, the same migration either succeeds in under a second because nothing was in its way, or it fails immediately and cleanly so you can retry it a minute later. What you never want is the third option: waiting, silently, gathering a crowd behind it.</p>
<h3>Row Locks Are a Separate, Smaller Story</h3>
<p>Everything above is about locking the table's structure. There's a second, unrelated locking system for locking individual rows of data, and it solves a different problem: two transactions trying to change the same row at the same time.</p>
<p>The classic example is a balance check before a debit. Read the balance, confirm there's enough money, then subtract the amount. Without a row lock, two concurrent withdrawals can both read the same starting balance and both proceed, and the account ends up wrong. Locking that one row for the duration of the transaction closes the gap.</p>
<p>There's also a neat variant built for queues: instead of locking a row and making every other worker wait for it, a worker can ask to skip any row that's already locked and grab the next free one instead. That's the difference between ten background workers piling up behind each other and ten workers each picking up a different job at the same instant.</p>
<h3>Deadlocks: When Two Correct Transactions Still Collide</h3>
<p>Occasionally two transactions each hold a lock the other one needs. Transaction A has locked account 1 and wants account 2. Transaction B has locked account 2 and wants account 1. Neither can proceed, and neither will ever let go voluntarily. PostgreSQL notices this after about a second, picks one of the two transactions, and kills it so the other can continue.</p>
<p>The fix isn't a database setting, it's a coding discipline: always acquire locks in the same order, everywhere in the codebase. If every transfer function locks the lower account ID first regardless of direction, the two transactions above stop being able to collide at all. It's the same trick as everyone in a crowded hallway agreeing to keep to the right.</p>
<h3>Locks With No Data Attached</h3>
<p>PostgreSQL also offers a kind of lock that has nothing to do with any table or row: an application-defined lock, identified by whatever number you choose. It's mainly used for one thing, making sure a background job runs on only one server at a time, even when there are several application instances that could all try to start it. Grab the lock, run the job, and if the process crashes mid-job, the lock releases itself automatically instead of leaving things stuck. It's a lighter, more reliable substitute for the Redis-based mutex a lot of teams reach for by default.</p>
<h3>The Takeaway</h3>
<p>A lock isn't something you fight, it's a ticket you're standing in line with, and most locking outages don't start with a bad lock. They start with someone who forgot to set a timeout and ended up holding the line for everyone behind them.</p>
<h3>Where This Fits in the Full Course</h3>
<p>This is Module 13 of the <a href="https://academy.jatinjainsaraf.com/postgresql-in-depth">PostgreSQL In-Depth course</a>, from the Architect phase covering the internals senior engineers eventually have to learn the hard way. If this scenario feels familiar, the earlier modules on <a href="https://academy.jatinjainsaraf.com/postgresql-in-depth/mvcc">MVCC</a> and transaction internals build the concurrency model this one depends on.</p>
<p>#PostgreSQL #Database #Backend #SQL #ProductionIncidents #SoftwareEngineering</p>]]></content:encoded>
      <pubDate>Wed, 15 Jul 2026 16:05:59 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Backend</category>
      <category>SQL</category>
      <category>Production Incidents</category>
    </item>
    <item>
      <title>What the PostgreSQL Server Is Actually Doing</title>
      <link>https://insight.jatinjainsaraf.com/what-the-postgresql-server-is-actually-doing</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/what-the-postgresql-server-is-actually-doing</guid>
      <description>When you run CREATE DATABASE, PostgreSQL creates a directory. When you run INSERT, it writes a row to a file and logs the write for crash safety. When you run SELECT, it reads that file back.</description>
      <content:encoded><![CDATA[<h1>What the PostgreSQL Server Is Actually Doing</h1>
<p>When you run <code>CREATE DATABASE</code>, PostgreSQL creates a directory. When you run <code>INSERT</code>, it writes a row to a file and logs the write for crash safety. When you run <code>SELECT</code>, it reads that file back. That's the whole trick, once you see it, the "magic" disappears.</p>
<hr>
<h3>What the PostgreSQL Server Is Actually Doing</h3>
<p>Most developers use PostgreSQL for years without ever forming a picture of what it's doing when a query runs. SQL goes in, rows come out, and everything in between feels like a sealed box. It isn't a sealed box. It's a program, running on a computer, reading and writing files, same as any other program you've written. This article builds that basic picture, in plain language, with no prior database knowledge assumed.</p>
<h3>A Database Is a Directory</h3>
<p>Run <code>CREATE DATABASE learning_postgres</code> and PostgreSQL doesn't do anything exotic. It creates a directory on disk to hold that database's data. That's it. Every database you create gets its own folder, sitting under PostgreSQL's data directory, the same way any application on your computer might create a folder to store its files.</p>
<p>This is worth sitting with, because it quietly answers a question that trips up a lot of people: "where does my data actually live?" It lives in a directory on the disk of whatever machine is running the PostgreSQL server. Not in the cloud in some abstract sense, not floating in "the database." On disk, in files, in a folder PostgreSQL controls.</p>
<p>It also explains something you may have noticed without thinking about it: <code>CREATE DATABASE</code> returns almost instantly, milliseconds, not minutes. That's because creating a database is just creating an empty directory. There's no data in it yet. The real work, and the real cost, only begins once rows start filling those files.</p>
<h3>A Table Is a File, a Row Is Bytes in That File</h3>
<p>Inside that directory, each table you create is backed by its own file (large tables get split across several, but the idea holds). When you run:</p>
<p>INSERT INTO users (name, email) VALUES ('Alice', '<a href="mailto:alice@example.com">alice@example.com</a>');</p>
<p>PostgreSQL takes that row and writes it, as bytes, into the file backing the <code>users</code> table. Nothing more mysterious than that.</p>
<p>When you run:</p>
<p>SELECT * FROM users WHERE email = '<a href="mailto:alice@example.com">alice@example.com</a>';</p>
<p>PostgreSQL opens that file (or consults an index that points into it, more on indexes in a later module), scans through the rows stored there, checks each one against your <code>WHERE</code> condition, and returns the ones that match.</p>
<p>Think of the file as a very disciplined spreadsheet that only PostgreSQL is allowed to touch directly. You never open it yourself, you always go through SQL, but structurally, that's what it is: rows, stored as bytes, in a file, on disk.</p>
<p>Under the hood, PostgreSQL is really answering a short chain of questions before it hands you an answer: Is the data I need already sitting in memory, or do I have to go to disk for it? Is there a shortcut, an index, that gets me there without reading the whole file? And once I've found a row, is it actually the current, correct version, or an old one nobody needs anymore? You don't write any of that logic. But knowing it happens is what lets you start reasoning about <em>why</em> one query is fast and an almost-identical one is slow.</p>
<h3>PostgreSQL Is a Process, Not a Black Box</h3>
<p>When you connect to PostgreSQL, whether from <code>psql</code>, a Node.js app, or a GUI tool, you're talking to a running process on a server. That process listens for your connection, hands you off to a dedicated worker just for your session, and from that point on, every query you send is handled by an ordinary program doing ordinary things: reading bytes from files, writing bytes to files, holding some of that data in memory so it doesn't have to touch the disk every single time.</p>
<p>That last part, keeping frequently-used data in memory, is why the <em>second</em> time you query something is usually much faster than the first. The process remembers what it recently read from disk and serves it from memory instead. This is the same idea as any caching you've done in application code, just built into the database itself.</p>
<p>This is also why a query can get slower for no reason you can find in your SQL. If PostgreSQL just restarted, deployed, crashed, rebooted, that memory is empty again. Nothing is cached yet. The first round of queries has to go back to disk to rebuild that cache from scratch, before things speed back up. If you've ever seen a database feel sluggish right after a restart and assumed you'd broken something, this is usually the real reason: the cache went cold, not your code.</p>
<h3>The Log That Saves You From Crashes</h3>
<p>Here's the one detail that separates "just writing to a file" from what a production database actually needs to guarantee: what happens if the power goes out, or the process crashes, in the middle of a write?</p>
<p>PostgreSQL's answer: before it changes the actual data file, it first writes down what it's <em>about to do</em>, to a separate log. Only after that note is safely on disk does it go ahead and make the real change.</p>
<p>If the server crashes mid-write, it doesn't matter, on restart, PostgreSQL reads that log and replays anything that didn't finish. It's the same instinct as keeping a to-do list before starting a task: if you get interrupted, you don't have to remember what you were doing, you just check the list. This log is one of the reasons people trust PostgreSQL with data they can't afford to lose.</p>
<h3>Why This Mental Model Matters</h3>
<p>Once "PostgreSQL is a process that reads and writes files, with a safety log protecting every write" is in your head, a lot of things that used to feel like separate pieces of magic start looking like variations on the same idea:</p>
<ul>
<li><strong>Indexes</strong> are just extra files that let PostgreSQL find the right rows without scanning the whole table.</li>
<li><strong>Caching (shared buffers)</strong> is just PostgreSQL keeping recently-used file contents in memory.</li>
<li><strong>Replication</strong> is just another server reading that same safety log and replaying it, to keep a second copy of the data in sync.</li>
<li><strong>VACUUM</strong> is just PostgreSQL cleaning up old row versions it no longer needs, so the files don't grow forever.</li>
</ul>
<p>None of these are separate systems bolted onto a mysterious core. They're all extensions of the same basic loop: read files, write files, log before you write, keep useful things in memory.</p>
<p>PostgreSQL isn't magic. It's a server process that manages files intelligently, and every advanced feature it has is that idea, applied more cleverly.</p>
<h3>Where This Fits in the Full Course</h3>
<p>This is the very first mental model from the Foundation phase of the <a href="https://academy.jatinjainsaraf.com/postgresql-in-depth">PostgreSQL In-Depth course</a>, the phase built for zero prior database knowledge. If this clicked, the next modules build on it directly: the client-server connection model in detail, the relational mental model (tables, rows, and how they connect), and your first complete schema.</p>
<p>For readers who want the advanced version of this same territory, process architecture, MVCC, the planner, replication internals, see <a href="/blog/the-postgresql-elephant-in-the-room-a-deep-dive-into-the-architecture-that-powers-giants">"The PostgreSQL Elephant in the Room."</a> That article assumes you already have the basic picture this one just gave you.</p>
<h3>The Question Worth Asking Yourself</h3>
<p>Next time a PostgreSQL query does something you don't expect, slow, fast, returning stale-looking data, try asking the simplest possible question first: what files is it reading or writing right now, and what would that look like from the outside?</p>
<p>You'll be surprised how often that question, on its own, points you toward the answer, long before you need to reach for anything more advanced.</p>
<p>#PostgreSQL #Database #Backend #SoftwareEngineering #DatabaseFundamentals #LearnToCode #SQL</p>]]></content:encoded>
      <pubDate>Sat, 11 Jul 2026 09:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>PostgreSQL</category>
      <category>Backend</category>
      <category>Fundamentals</category>
    </item>
    <item>
      <title>Competition Is Inevitable. Cruelty Is Optional</title>
      <link>https://insight.jatinjainsaraf.com/competition-is-inevitable-cruelty-is-optional</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/competition-is-inevitable-cruelty-is-optional</guid>
      <description>Every promotion in tech has an invisible downside. When someone becomes a Senior Developer, someone else waits another review cycle. That&apos;s the reality of a competitive industry. But success isn&apos;t abo</description>
      <content:encoded><![CDATA[<h1>Competition Is Inevitable. Cruelty Is Optional.</h1>
<p>Every promotion in tech has an invisible downside.</p>
<hr>
<ul>
<li>When someone becomes an Intern, thousands of applicants don't.</li>
<li>When someone becomes a Junior Developer, another candidate receives a rejection email.</li>
<li>When someone becomes a Senior Developer, someone else waits another review cycle.</li>
<li>When someone becomes a Tech Lead or Engineering Manager, dozens of equally ambitious engineers aren't selected.</li>
</ul>
<p>That's the reality of a competitive industry.</p>
<p>But here's where many people get it wrong.</p>
<p>Success isn't about destroying people. It's about becoming the best choice.</p>
<p>You don't need to sabotage a colleague.
You don't need office politics.
You don't need to hope others fail.</p>
<p>You need to build skills that make the decision obvious.</p>
<ul>
<li>Learn continuously.</li>
<li>Take ownership.</li>
<li>Communicate clearly.</li>
<li>Deliver consistently.</li>
<li>Solve bigger problems than yesterday.</li>
</ul>
<p>Competition is inevitable.
Cruelty is optional.</p>
<p>The goal isn't to terminate careers. The goal is to become so valuable that opportunities naturally come your way.</p>
<p>In the end, the market doesn't reward the loudest engineer.</p>
<p>It rewards the one who consistently creates the most value.</p>
<p>Outperform the competition. Respect the competitors.</p>
<p>#SoftwareEngineering #CareerGrowth #Leadership #TechCareers #Engineering #ContinuousLearning #TechLeadership</p>]]></content:encoded>
      <pubDate>Tue, 30 Jun 2026 14:47:34 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Career</category>
      <category>Growth</category>
      <category>Leadership</category>
    </item>
    <item>
      <title>Transactions and ACID in Practice: What Every Backend Developer Must Know</title>
      <link>https://insight.jatinjainsaraf.com/transactions-and-acid-in-practice-what-every-backend-developer-must-know</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/transactions-and-acid-in-practice-what-every-backend-developer-must-know</guid>
      <description>The money left Alice&apos;s account. Bob never received it. The server crashed in between. If you don&apos;t understand transactions, this is how your application loses data, silently, permanently, with no erro</description>
      <content:encoded><![CDATA[<h1>Transactions and ACID in Practice: What Every Backend Developer Must Know</h1>
<p>The money left Alice's account. Bob never received it. The server crashed in between. If you don't understand transactions, this is how your application loses data, silently, permanently, with no error log.</p>
<hr>
<h3>Transactions and ACID in Practice: What Every Backend Developer Must Know</h3>
<p>Here is the scenario that every backend developer eventually hits in production. A bank transfer: debit $100 from Alice, credit $100 to Bob. Two SQL statements. Simple.</p>
<p>The server crashes between them.</p>
<p>Alice has lost $100. Bob never received it. The money has vanished into the gap between two database writes, and your application has no idea it happened.</p>
<p>This is exactly the problem transactions were invented to solve. And yet, after years of reviewing code and debugging production incidents, I can tell you: most developers use transactions far less than they should, misunderstand what isolation levels actually do, and write patterns that hold locks for seconds longer than necessary.</p>
<p>This article covers the full mental model, BEGIN, COMMIT, ROLLBACK, all four isolation levels, SELECT FOR UPDATE, SKIP LOCKED for job queues, and the most common anti-patterns that silently destroy performance at scale. It's based directly on the Transactions &#x26; ACID in Practice module from the PostgreSQL In-Depth course.</p>
<h3>The Problem Transactions Solve</h3>
<p>Consider the transfer code without transactions:</p>
<p>UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;</p>
<p>If anything interrupts execution between those two lines, a server crash, an application exception, a network timeout, Alice's account has been debited but Bob's has not been credited. The database is now in an inconsistent state, and it has no knowledge that anything went wrong.</p>
<p>Transactions fix this by grouping operations into an atomic unit: either all of them succeed, or none of them do.</p>
<p>BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;</p>
<p>If the server crashes after the first UPDATE and before COMMIT, PostgreSQL automatically rolls back the entire transaction on restart. Alice keeps her $100. The database never reaches a half-updated state.</p>
<h3>What ACID Actually Means</h3>
<p>ACID is not a marketing acronym. Each letter describes a specific guarantee the database makes, and understanding each one changes how you write application code.</p>
<p><strong>Atomicity</strong> means all operations in a transaction succeed or none do. There is no partial success. If your transaction debits Alice and the next statement fails, the debit is undone.</p>
<p><strong>Consistency</strong> means a transaction takes the database from one valid state to another. Constraints, NOT NULL, FOREIGN KEY, CHECK, are enforced at commit time. If you have a CHECK (balance >= 0) constraint and a debit would push a balance negative, the entire transaction fails at COMMIT and rolls back automatically. The database enforces your business rules, not just your application code.</p>
<p><strong>Isolation</strong> means concurrent transactions run as if they are the only transaction in the system. Another session's uncommitted changes are invisible to you. If Session A is updating a row and hasn't committed yet, Session B reads the pre-update value, not the in-progress value.</p>
<p><strong>Durability</strong> means once committed, data survives crashes. PostgreSQL's Write-Ahead Log (WAL) ensures this, every committed transaction is written to durable storage before the COMMIT acknowledgement is returned to the client.</p>
<h3>The Three Transaction Commands</h3>
<p>BEGIN starts a transaction. Without it, every statement is its own transaction, it auto-commits immediately. This is fine for single statements, dangerous for multi-step operations.</p>
<p>COMMIT makes all changes permanent and releases locks.</p>
<p>ROLLBACK undoes all changes since BEGIN and releases locks. If your session disconnects before COMMIT, PostgreSQL automatically rolls back.</p>
<p>In application code (Node.js example):</p>
<pre><code class="language-javascript">const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 1]);
  await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 2]);
  await client.query('COMMIT');
} catch (err) {
  await client.query('ROLLBACK');
  throw err;
} finally {
  client.release();
}
</code></pre>
<p>The <code>finally</code> block is critical, always release the connection back to the pool, whether the transaction succeeded or failed.</p>
<p>One thing many developers don't realise: once an error occurs inside a transaction in PostgreSQL, the transaction is <strong>aborted</strong>. Every subsequent statement in that transaction will fail with "current transaction is aborted, commands ignored until end of transaction block", until you issue a ROLLBACK. This has caught many teams off guard in production.</p>
<h3>The Four Isolation Levels</h3>
<p>PostgreSQL supports four isolation levels. Understanding the difference is not academic, choosing the wrong one causes data integrity bugs that are extremely hard to debug.</p>
<p><strong>READ COMMITTED (the default)</strong>, each query in your transaction sees a fresh snapshot of committed data at the time that query runs. This means two identical SELECT queries in the same transaction can return different results if another transaction commits between them. This is called a non-repeatable read, and it is expected behaviour at this level. For most application work, READ COMMITTED is correct.</p>
<p><strong>REPEATABLE READ</strong>, the entire transaction sees the same snapshot of data as of when the transaction started. If another session inserts 100 rows and commits while your transaction is open, your subsequent queries still see the original row count. Use this when you need consistency across multiple queries in a single transaction, for example, generating a financial report where all queries must reflect the same point in time.</p>
<p><strong>SERIALIZABLE</strong>, the strongest level. Transactions execute as if they were serialised one after another. PostgreSQL detects situations where concurrent transactions could produce results different from any serial execution and fails one of them with a serialization error. Use this sparingly, it adds overhead and requires retry logic in your application.</p>
<p><strong>READ UNCOMMITTED</strong>, in theory this allows dirty reads (reading uncommitted data from other transactions). In practice, PostgreSQL treats it identically to READ COMMITTED. Postgres never allows dirty reads, regardless of isolation level.</p>
<p>The rule for most applications: use READ COMMITTED for everything. Move to REPEATABLE READ only when you genuinely need a consistent multi-query snapshot. Use SERIALIZABLE only for complex financial operations where you've reasoned through why lower isolation levels produce incorrect results.</p>
<h3>SELECT FOR UPDATE: Pessimistic Locking</h3>
<p>Here is a classic race condition. Two workers both check an account balance, both decide it's sufficient, and both proceed to debit, leaving the balance negative despite a check constraint.</p>
<p>The naïve code:</p>
<p>SELECT balance FROM accounts WHERE id = 1;
-- application checks: if balance >= 100, proceed
UPDATE accounts SET balance = balance - 100 WHERE id = 1;</p>
<p>Between the SELECT and the UPDATE, another transaction can change the balance. Your check was valid when you made it and invalid when the UPDATE runs.</p>
<p>The fix is SELECT FOR UPDATE, which locks the row at read time:</p>
<p>BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- Row is now locked. No other transaction can modify it until we commit.
-- Application checks balance, decides to proceed
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;</p>
<p>Any other transaction trying to SELECT FOR UPDATE or UPDATE the same row will wait until you commit or rollback. The row is protected for the entire check-then-modify sequence.</p>
<h3>FOR UPDATE SKIP LOCKED: The Job Queue Pattern</h3>
<p>FOR UPDATE SKIP LOCKED is one of the most useful concurrency primitives PostgreSQL offers, and it is almost unknown outside of experienced backend teams.</p>
<p>The scenario: a table of background jobs, multiple workers running concurrently, each trying to claim and process the next available job. Using plain FOR UPDATE causes workers to queue up waiting for the same row lock, defeating the purpose of concurrency. Using no locking at all causes multiple workers to claim the same job.</p>
<p>SKIP LOCKED solves this exactly:</p>
<p>BEGIN;
SELECT id, payload
FROM jobs
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;</p>
<p>UPDATE jobs SET status = 'processing', worker_id = $worker WHERE id = $id;
COMMIT;</p>
<p>Each worker instantly skips rows that are already locked by another worker and claims the next available one. No blocking, no duplicate processing. This is the correct pattern for any queue-based workload in PostgreSQL.</p>
<h3>The Most Expensive Mistake: Long Transactions</h3>
<p>This is the pattern I see most often in production codebases, and it causes damage that compounds over time.</p>
<pre><code class="language-javascript">// ❌ Anti-pattern: keeping a transaction open during a network call
await client.query('BEGIN');
const orders = await client.query("SELECT * FROM orders WHERE status = 'pending'");

// Make a payment API call, takes 2-5 seconds
const result = await paymentGateway.charge(orders.rows[0]);

await client.query('UPDATE orders SET status = $1 WHERE id = $2', ['confirmed', orders.rows[0].id]);
await client.query('COMMIT');
</code></pre>
<p>What's happening during those 2-5 seconds:</p>
<ul>
<li>Locks are held on every row the transaction has touched.</li>
<li>Other transactions trying to modify those rows are blocked.</li>
<li>PostgreSQL cannot vacuum dead tuples from any table involved in the transaction, across the entire database, not just the rows you locked.</li>
<li>Connection pool slots are occupied for the full duration.</li>
</ul>
<p>At low traffic, this is invisible. At 100 concurrent requests with 3-second payment API calls, you have 100 transactions holding locks simultaneously. Autovacuum falls behind. Tables bloat. Queries slow down. The cause is nearly impossible to identify without knowing what to look for.</p>
<p>The correct pattern:</p>
<pre><code class="language-javascript">// ✅ Query outside the transaction
const orders = await pool.query("SELECT * FROM orders WHERE status = 'pending'");

// Do the slow external work, no locks held
const result = await paymentGateway.charge(orders.rows[0]);

// Open a short transaction only for the write
const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query('UPDATE orders SET status = $1 WHERE id = $2', ['confirmed', orders.rows[0].id]);
  await client.query('COMMIT');
} catch (err) {
  await client.query('ROLLBACK');
  throw err;
} finally {
  client.release();
}
</code></pre>
<p>Transaction open for milliseconds, not seconds. Lock held for milliseconds. Autovacuum unaffected.</p>
<h3>SAVEPOINT: Partial Rollback</h3>
<p>A SAVEPOINT lets you roll back part of a transaction without abandoning the whole thing. This is useful when you have a series of operations where one failing step should be retried or skipped, not the entire transaction.</p>
<p>BEGIN;</p>
<p>INSERT INTO orders (customer_id, total) VALUES (1, 150.00);</p>
<p>SAVEPOINT before_items;</p>
<p>INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 999, 1);
-- Fails: product 999 doesn't exist</p>
<p>ROLLBACK TO SAVEPOINT before_items;
-- The order INSERT is preserved. The failed order_item INSERT is undone.</p>
<p>INSERT INTO order_items (order_id, product_id, quantity) VALUES (1, 2, 1);
-- Correct product this time</p>
<p>COMMIT;
-- Order + 1 item committed. The failed attempt left no trace.</p>
<h3>Where This Fits in the Full Course</h3>
<p>The patterns above, along with isolation level trade-offs, deadlock prevention, and the WAL durability mechanics behind durability, are one module of the PostgreSQL In-Depth course at academy.jatinjainsaraf.com.</p>
<p>The course covers three phases: Foundation (absolute basics for beginners, zero assumed knowledge), Practitioner (the patterns working engineers use every week, indexes, schema design, JSONB, full-text search, performance tuning), and Architect (MVCC internals, WAL, autovacuum mechanics, replication, partitioning, and connection pooling failure modes).</p>
<p>Every module is built from years of running PostgreSQL on a live blockchain indexer whose database grew to 10 TB. The examples in this article are from real production scenarios, not documentation.</p>
<h3>The Questions Worth Asking About Your Own Codebase</h3>
<p>Do any of your API endpoints hold a database transaction open while making an external HTTP call? Even one that takes 200ms?</p>
<p>Do your background job workers use FOR UPDATE SKIP LOCKED, or are they competing for the same row lock?</p>
<p>What happens in your application when a transaction fails mid-way? Does your error handling issue ROLLBACK and release the connection, or does it leave an aborted transaction sitting in the pool?</p>
<p>Do you know which isolation level your most critical queries are running at, and whether that level is actually appropriate for what those queries are doing?</p>
<p>These are not theoretical questions. Each one has a production failure mode attached to it. The good news is that once you have the mental model, the fixes are straightforward.</p>
<p>#PostgreSQL #Database #Backend #SQL #SoftwareEngineering #DatabaseEngineering #Transactions #ACID #NodeJS #BackendDevelopment</p>]]></content:encoded>
      <pubDate>Fri, 26 Jun 2026 18:34:06 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>PostgreSQL</category>
      <category>Backend</category>
      <category>SQL</category>
    </item>
    <item>
      <title>When Judgment Becomes the Bottleneck</title>
      <link>https://insight.jatinjainsaraf.com/when-judgment-becomes-the-bottleneck</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/when-judgment-becomes-the-bottleneck</guid>
      <description>Senior engineers get rewarded for good judgment, until that judgment becomes the thing every decision waits on. Here&apos;s how to recognize when you&apos;ve become the bottleneck, and what to do about it.</description>
      <content:encoded><![CDATA[<p>There's a moment in most senior engineers' careers that nobody warns you about.</p>
<p>You've earned trust. Your instincts are sharp.
People come to you before committing to a design. Before merging a PR. Before choosing a database. Before making a call.</p>
<p>And slowly, without noticing it, you stop being an engineer and start being a gate.</p>
<h2>The trap looks like success.</h2>
<p>You're being consulted because your judgment is valued.
You're in every important meeting because your perspective matters.
You're the person who "gets it."</p>
<p>But here's what's actually happening:</p>
<p>PRs sit waiting for your review.
Decisions stall until you're available.
Engineers stop thinking through options, because they'll just ask you anyway.
Your calendar fills with meetings that exist to get your approval.</p>
<p>The team isn't scaling. It's depending.</p>
<h2>This is what judgment-as-bottleneck looks like:</h2>
<ol>
<li>Velocity is tied to your availability, not the team's capacity</li>
<li>Junior engineers learn your answers, not your reasoning</li>
<li>Decisions that could have been made in an hour wait three days</li>
<li>You feel indispensable, but the system is actually fragile</li>
</ol>
<p>The irony? The more you care about quality, the more likely you are to accidentally create this pattern.</p>
<h2>The fix isn't doing less. It's building judgment transfer.</h2>
<p>Share the <em>why</em> behind decisions, not just the decision.
Write down the heuristics you use, the instincts you've built over years deserve to be externalised.
Define decision boundaries: "You own anything under this scope. Come to me only when it crosses these lines."
Let people make calls you'd have made differently, then debrief rather than override.</p>
<p>The best engineering leaders I've seen don't protect quality by owning every decision.
They protect it by raising the quality of how the team decides.</p>
<p>Your judgment shouldn't be the ceiling. It should be the foundation.</p>
<hr>
<p>When you make yourself the bottleneck, even unintentionally, you're not protecting quality.
You're just delaying it.</p>
<p>The real work isn't being right every time.
It's building a team that can be right when you're not in the room.</p>
<p>💬 Have you ever caught yourself becoming the bottleneck,  or working under one?</p>
<p>#EngineeringLeadership #TechLead #SoftwareEngineering #LeadershipMindset #TeamScaling</p>]]></content:encoded>
      <pubDate>Sun, 21 Jun 2026 17:29:30 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Leadership</category>
      <category>Tech Lead</category>
      <category>Software Engineering</category>
      <category>Team Scaling</category>
    </item>
    <item>
      <title>Mastering PostgreSQL: 5 Essential Tips for Performance Optimization</title>
      <link>https://insight.jatinjainsaraf.com/mastering-postgresql-5-essential-tips-for-performance-optimization</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/mastering-postgresql-5-essential-tips-for-performance-optimization</guid>
      <description>When your database slows down, your whole application suffers. Discover 5 essential PostgreSQL optimization tips—from EXPLAIN ANALYZE to smart indexing—that will dramatically improve your query perfor</description>
      <content:encoded><![CDATA[<p>If you've been working with web applications for a while, you know that the database is often the central nervous system of your app. When your database slows down, everything slows down. Having recently completed a deep dive into PostgreSQL, I wanted to share five essential optimization techniques that can dramatically improve your query performance and application scaling.</p>
<p>Let's dive into some practical tips for keeping your PostgreSQL database lightning fast!</p>
<h3>1. Understand Your Queries with <code>EXPLAIN ANALYZE</code></h3>
<p>Before you can optimize a slow query, you need to know <em>why</em> it's slow. PostgreSQL provides an incredibly powerful tool for this: <code>EXPLAIN</code>. By prepending <code>EXPLAIN ANALYZE</code> to your query, Postgres won't just run the query—it will give you a detailed execution plan showing exactly how it fetched the data.</p>
<pre><code class="language-sql">EXPLAIN ANALYZE 
SELECT * FROM users WHERE last_login > '2023-01-01';
</code></pre>
<p>Look for "Seq Scan" (Sequential Scan) in the output. If you see it on a large table, it means Postgres is scanning every single row to find your data. This is your cue that an index might be needed!</p>
<h3>2. Index Smartly (But Don't Over-Index)</h3>
<p>Indexes are the most common way to speed up read queries. The B-Tree index is the default and works perfectly for equality and range queries.</p>
<pre><code class="language-sql">CREATE INDEX idx_users_last_login ON users(last_login);
</code></pre>
<p>However, a common beginner mistake is indexing <em>every</em> column. Remember that every time you <code>INSERT</code>, <code>UPDATE</code>, or <code>DELETE</code> a row, Postgres has to update the indexes too. Too many indexes will slow down your write operations. Only index columns that are frequently used in <code>WHERE</code>, <code>JOIN</code>, or <code>ORDER BY</code> clauses.</p>
<h3>3. Leverage the Power of JSONB</h3>
<p>One of PostgreSQL's most beloved features is its native support for JSON. But did you know there are two types: <code>json</code> and <code>jsonb</code>?</p>
<p>Always default to <code>jsonb</code>. While <code>json</code> stores an exact copy of the input text, <code>jsonb</code> stores the data in a decomposed binary format. This makes it slightly slower to insert, but significantly faster to process. Even better, you can index <code>jsonb</code> fields!</p>
<pre><code class="language-sql">-- Creating a GIN index on a jsonb column
CREATE INDEX idx_user_metadata ON users USING GIN (metadata);
</code></pre>
<h3>4. Vacuum Regularly</h3>
<p>PostgreSQL uses Multiversion Concurrency Control (MVCC) to handle simultaneous transactions. When you update or delete a row, Postgres doesn't immediately remove the old version—it marks it as a "dead tuple". Over time, these dead tuples cause database bloat.</p>
<p>The <code>VACUUM</code> process cleans up these dead tuples. While Postgres has an <code>autovacuum</code> daemon that handles this automatically, heavily updated tables might need customized autovacuum settings. Keep an eye on your dead tuple counts; if they grow too large, performance will degrade.</p>
<h3>5. Use Connection Pooling</h3>
<p>Every new connection to PostgreSQL spins up a new OS process, which consumes around 10MB of memory. If your web application opens a new connection for every request, you'll quickly exhaust your server's RAM and CPU.</p>
<p>Instead, use a connection pooler like <strong>PgBouncer</strong> or <strong>Pgpool-II</strong>. These tools maintain a pool of active database connections and share them among your application's requests, drastically reducing the overhead on the database server.</p>
<h3>Conclusion</h3>
<p>PostgreSQL is a massive, feature-rich database engine. While it works beautifully out of the box, understanding how it executes queries, manages memory, and handles concurrency will elevate you from a simple user to a database master.</p>
<p>Try running <code>EXPLAIN ANALYZE</code> on your slowest queries today—you might be surprised by what you find!</p>]]></content:encoded>
      <pubDate>Thu, 11 Jun 2026 16:27:25 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Performance</category>
      <category>SQL</category>
      <category>Backend</category>
    </item>
    <item>
      <title>Redis HyperLogLog: Counting Millions of Unique Users with Just 12KB of Memory</title>
      <link>https://insight.jatinjainsaraf.com/redis-hyperloglog-counting-millions-of-unique-users-with-just-12kb-of-memory</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/redis-hyperloglog-counting-millions-of-unique-users-with-just-12kb-of-memory</guid>
      <description>Discover how Redis HyperLogLog efficiently estimates the number of unique elements in massive datasets, using a tiny 12KB memory footprint with impressive accuracy. Learn its probabilistic approach an</description>
      <content:encoded><![CDATA[<p>When building at scale, answering a seemingly simple question like <em>"How many unique users visited our site today?"</em> can become a massive engineering challenge.</p>
<p>If you use a traditional <code>Set</code> data structure to store every unique ID, tracking millions of users will rapidly consume Gigabytes of RAM. This is where <strong>Redis HyperLogLog</strong> comes to the rescue.</p>
<h2>What is HyperLogLog?</h2>
<p>HyperLogLog (HLL) is a probabilistic data structure used to estimate the cardinality of a set (the number of unique elements). Instead of storing the actual elements, it hashes them and observes the patterns of the binary representation of the hash.</p>
<p>The magic of Redis' implementation of HyperLogLog is that it can estimate the cardinality of millions of unique items while <strong>always using a maximum of 12 KB of memory</strong> and maintaining a standard error of just <code>0.81%</code>.</p>
<h2>How it works (The Math made simple)</h2>
<p>Imagine you are flipping a coin. If you flip it and get "Heads" 5 times in a row, you'd intuitively know you've probably been flipping that coin for a while.</p>
<p>HLL works on a similar principle using hashing:</p>
<ol>
<li>Every item added to the HLL is hashed into a large binary string (e.g., <code>101100101...</code>).</li>
<li>The algorithm looks for the <strong>longest sequence of leading zeros</strong> in these binary strings.</li>
<li>If the longest sequence of leading zeros across all hashes is <code>N</code>, the estimated number of unique elements is roughly <code>2^N</code>.</li>
</ol>
<p>To reduce variance and make the estimate incredibly accurate, Redis divides the data into <code>16,384</code> internal registers and averages the results using harmonic means. This brings the memory footprint to exactly 16384 × 6 bits = 12 KB.</p>
<h2>Practical Usage in Redis</h2>
<p>Using HLL in Redis is delightfully simple. It provides three main commands:</p>
<h3>1. PFADD</h3>
<p>Adds elements to the HyperLogLog.</p>
<pre><code class="language-bash">PFADD website_visitors:2026-06-06 "user_102" "user_883" "user_911"
# Returns 1 if the internal register was altered
</code></pre>
<h3>2. PFCOUNT</h3>
<p>Returns the approximated cardinality.</p>
<pre><code class="language-bash">PFCOUNT website_visitors:2026-06-06
# Returns: 3
</code></pre>
<h3>3. PFMERGE</h3>
<p>Merges multiple HyperLogLogs into a single one. This is perfect for rolling up daily metrics into weekly or monthly metrics!</p>
<pre><code class="language-bash">PFMERGE visitors:this_week visitors:monday visitors:tuesday visitors:wednesday
</code></pre>
<h2>When to use HyperLogLog</h2>
<p>HyperLogLog is not a silver bullet. You should use it when:</p>
<ul>
<li>You need to count massive amounts of unique items (IP addresses, user IDs, search queries).</li>
<li>You care about memory efficiency more than absolute 100% precision.</li>
<li>You do <strong>not</strong> need to retrieve the actual items back from the data structure.</li>
</ul>
<p>If you need to list the users, or if absolute precision is required for billing purposes, stick to a standard Redis <code>Set</code> or an SQL database. But for analytical dashboards and scale, HyperLogLog is an absolute superpower.</p>]]></content:encoded>
      <pubDate>Sat, 06 Jun 2026 07:49:29 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Redis</category>
      <category>HyperLogLog</category>
      <category>Cardinality</category>
      <category>Probabilistic Data Structure</category>
      <category>Memory Efficiency</category>
    </item>
    <item>
      <title>Microservices Don&apos;t Collapse Because They&apos;re &quot;Too Complex&quot;</title>
      <link>https://insight.jatinjainsaraf.com/microservices-system-design-software-architecture</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/microservices-system-design-software-architecture</guid>
      <description>Microservices don’t collapse because they’re “too complex.”</description>
      <content:encoded><![CDATA[<h1>Microservices Don't Collapse Because They're "Too Complex"</h1>
<p>Microservices don’t collapse because they’re “too complex.”</p>
<p>They collapse because teams slowly recreate a monolit,  just distributed.</p>
<p>On diagrams, everything looks clean:
Neat boxes. Clear boundaries. Perfect arrows.</p>
<p>In production?
That’s where the truth shows up.</p>
<p>Services calling each other in long synchronous chains.
Retries multiplying failures instead of absorbing them.
Shared databases quietly coupling releases.
Gateways absorbing business logic.
Timeouts missing.
Ownership unclear.</p>
<p>Nothing fails dramatically at first.</p>
<p>It just gets slower.
Harder to change.
Riskier to deploy.</p>
<p>And then one day, a small issue becomes a cascading outage.</p>
<p>Microservices demand discipline more than architecture.</p>
<p>They require:</p>
<p>– Clear domain boundaries
– Explicit ownership
– Versioning strategy
– Observability by default
– Thoughtful retry + timeout policies
– An actual consistency model
– Automation everywhere</p>
<p>Without that, you don’t have microservices.</p>
<p>You have a distributed monolith with extra latency.</p>
<p>Microservices don’t need more services.
They need better engineering judgment.</p>
<p>Architecture doesn’t fail on paper.
It fails in habits.</p>
<p>#Microservices #SystemDesign #SoftwareArchitecture #DistributedSystems #BackendEngineering #EngineeringLeadership #DevOps</p>]]></content:encoded>
      <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Engineering</category>
      <category>System Design</category>
      <category>Design</category>
    </item>
    <item>
      <title>Professionalism Isn&apos;t Rigidity — It&apos;s Responsibility</title>
      <link>https://insight.jatinjainsaraf.com/work-culture-professionalism-team-communication</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/work-culture-professionalism-team-communication</guid>
      <description>Professionalism Isn’t Rigidity ==&gt; It’s Responsibility</description>
      <content:encoded><![CDATA[<h1>Professionalism Isn't Rigidity — It's Responsibility</h1>
<p>Professionalism Isn’t Rigidity ==> It’s Responsibility</p>
<p>In every organisation, work flows through multiple people, teams, and systems.
And whenever that happens, one principle becomes essential:</p>
<p>Clear communication isn’t bureaucracy.
It’s accountability.</p>
<p>Documenting a decision in the right channel, informing the relevant teams, or keeping updates transparent, it’s not about “being rigid” or “too formal.”
It’s about ensuring that:
•	Nothing gets lost,
•	No one gets blindsided, and
•	Everyone has the same context.</p>
<p>In tech especially, a small correction in the wrong place can trigger a big impact elsewhere.
A quiet change might fix a momentary problem, but a documented change prevents future ones.</p>
<p>Professionalism doesn’t mean saying “no.”
It means saying “let’s make sure this is visible so the whole team stays aligned.”</p>
<p>Because when work affects users, systems, or data…
clarity is not optional, it’s responsibility.</p>
<p>💬 What’s one communication practice you feel every team should follow?</p>
<p>#WorkCulture #Professionalism #TeamCommunication #EngineeringMindset</p>]]></content:encoded>
      <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Insights</category>
    </item>
    <item>
      <title>Ownership Mindset: Thinking Beyond Assigned Tasks</title>
      <link>https://insight.jatinjainsaraf.com/ownership-mindset-leadership-software-engineering</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/ownership-mindset-leadership-software-engineering</guid>
      <description>Ownership Mindset: Thinking Beyond Assigned Tasks</description>
      <content:encoded><![CDATA[<h1>Ownership Mindset: Thinking Beyond Assigned Tasks</h1>
<p>🧠 Ownership Mindset: Thinking Beyond Assigned Tasks</p>
<p>One thing I’ve learned in engineering (and honestly, in life) is this:</p>
<p>Anyone can complete tasks.
Very few take ownership.</p>
<p>Ownership isn’t about working more.
It’s about thinking deeper.</p>
<p>It means asking questions like:</p>
<p>“What is the real goal behind this task?”
“Is there a better or faster way to achieve it?”
“What could break later if we don’t address it now?”
“Is this decision aligned with the bigger picture?”</p>
<p>When you operate with an ownership mindset, your work shifts from executing tickets to building outcomes. You stop thinking, “This part isn’t my responsibility,” and start thinking, “How can I make this better for the team, the users, and the system?”</p>
<p>Signs you’re moving from task-based to ownership-based thinking:</p>
<p>You anticipate problems before they show up.
You fill gaps even if they’re “not assigned to you.”
You communicate proactively instead of reactively.
You care about long-term impact, not just short-term delivery.
You look at systems holistically, not just your module.</p>
<p>In a world full of skilled developers, the ones who stand out are not just good coders,  they’re good owners. They treat the product like something they built, not something they were merely told to work on.</p>
<p>If you want to grow faster, gain trust, and build influence…
Start thinking beyond your tasks. Start thinking like an owner.</p>
<p>#OwnershipMindset #Leadership #SoftwareEngineering #CareerGrowth #ProblemSolving #TechMindset #EngineeringCulture #ProfessionalGrowth #Responsibility #MindsetMatters #DevelopersJourney #Teamwork #WorkEthic #ProductThinking</p>]]></content:encoded>
      <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Insights</category>
      <category>Leadership</category>
    </item>
    <item>
      <title>The React Server Components Vulnerability That Shocked the Web Ecosystem</title>
      <link>https://insight.jatinjainsaraf.com/react-js-next-js-security</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/react-js-next-js-security</guid>
      <description>(Dec 2025)</description>
      <content:encoded><![CDATA[<h1>The React Server Components Vulnerability That Shocked the Web Ecosystem</h1>
<p>🚨 The React Server Components Vulnerability That Shocked the Web Ecosystem (Dec 2025)</p>
<p>In the last few weeks, the React community faced one of its most serious security incidents, a set of vulnerabilities in React Server Components (RSC) that exposed thousands of production systems to Remote Code Execution (RCE), source-code leakage, and DoS attacks.</p>
<p>Not just another CVE.
A wake-up call for every team using modern full-stack React frameworks.</p>
<p>🔥 What Actually Happened?
In December 2025, researchers uncovered critical issues in the react-server-dom-* packages used by React 19 and frameworks like Next.js:</p>
<p>1️⃣ CVE-2025-55182: Remote Code Execution (Critical)
Attackers could trigger server-side code execution through crafted RSC requests, without authentication.
This enabled full server takeover, malware deployment, data extraction, and infrastructure compromise.
It was actively exploited by advanced threat groups.</p>
<p>2️⃣ CVE-2025-55184: Denial of Service
Malformed payloads could crash RSC handlers and bring applications down instantly.</p>
<p>3️⃣ CVE-2025-55183: Source Code Exposure
Some setups mistakenly returned server function source code to clients, exposing business logic and internal secrets.</p>
<p>🌍 Global Impact
The impact was widespread:
🔹 600,000+ domains were found vulnerable,  from startups to enterprise SaaS.
🔹 Exploitation was confirmed against cloud apps, fintech dashboards, admin portals, and misconfigured Next.js deployments.
🔹 Next.js App Router apps were hit hardest, since Server Actions expanded the execution surface.
🔹 Cloud providers issued emergency advisories, rolled out WAF rules, and scanned hosted projects.
🔹 Many engineering teams had to rotate secrets, rebuild servers, and audit logs, because an RCE incident leaves long-term risk even after patching.</p>
<p>🛡️ How to Mitigate Right Now</p>
<p>If you use React Server Components (React 19, Next.js App Router, or any react-server-dom-* package):</p>
<p>✔️ Update to patched RSC versions — 19.0.1 / 19.1.2 / 19.2.1+
✔️ Update your framework (Next.js, Remix, etc.) to their security releases
✔️ Restrict public access to server actions where possible
✔️ Add WAF rules to block malicious RSC payloads
✔️ Audit logs since Nov 1 for:
• Unknown restarts
• Suspicious outbound calls
• Unexpected files on disk</p>
<p>💬 Final Thought</p>
<p>React Server Components are powerful, but this incident highlighted a deeper truth:
When JavaScript becomes your backend, your frontend developers become part of your security surface.
This vulnerability shook the ecosystem, but it also pushed the community toward safer patterns and stronger security awareness.
If your application uses React or Next.js in production, patching isn’t optional; it’s urgent.</p>
<p>#ReactJS #Nextjs #Security #WebSecurity #JavaScript #DevSecOps #EngineeringLeadership #FullStackDevelopment</p>]]></content:encoded>
      <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Engineering</category>
    </item>
    <item>
      <title>A Redis Command That Quietly Solves One of the Hardest Problems in High-Concurrency Systems</title>
      <link>https://insight.jatinjainsaraf.com/redis-backend-development</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/redis-backend-development</guid>
      <description>HINCRBYFLOAT.</description>
      <content:encoded><![CDATA[<h1>A Redis Command That Quietly Solves One of the Hardest Problems in High-Concurrency Systems</h1>
<p>A Redis command that quietly solves one of the hardest problems in high-concurrency systems.</p>
<p>I was diving into the Redis documentation recently and found myself appreciating a highly powerful command: <strong>HINCRBYFLOAT</strong>.</p>
<p>If you're building real-time analytics, IoT data pipelines, or ad-tech platforms, this command is an absolute lifesaver. It allows you to target a specific field inside a Redis Hash and increment (or decrement, with negative numbers) its decimal value in a single step.</p>
<h2>Why It's a Game-Changer for High-Concurrency Systems</h2>
<h3>1️⃣ 100% Atomic Operations</h3>
<p>It completely eliminates the risky "Read-Modify-Write" cycle in your application logic. Redis handles the math in-memory, ensuring zero race conditions even under millions of simultaneous requests.</p>
<h3>2️⃣ Automatic Upserts</h3>
<p>No need to check if the data exists first. If the hash or field doesn't exist, Redis initializes it at 0 and applies the increment on the fly.</p>
<h3>3️⃣ Massive Performance Gain</h3>
<p>It reduces network round-trips and complex database locking mechanisms down to a single, lightning-fast line of code.</p>
<h2>The Engineering Catch ⚠️</h2>
<p>Because it relies on standard IEEE 754 double-precision floating-point math, it can introduce those classic, tiny decimal rounding errors over time (think 0.30000000000000004). It also strips trailing zeros, returning 5.00 as "5".</p>
<h2>The Takeaway</h2>
<p><strong>For high-frequency aggregations, dashboards, and sensor metrics?</strong> HINCRBYFLOAT is perfect.</p>
<p>But if you're dealing with <strong>precise ledger data or financial transactions</strong> where every micro-penny counts, the industry gold standard is still to multiply your values by 100, store them as integers (cents), and stick to the classic HINCRBY.</p>
<p>It's a great reminder that every powerful tool comes with architectural trade-offs!</p>
<hr>
<p><strong>Tags:</strong> #SoftwareEngineering #Redis #BackendDevelopment #SystemDesign #Database #Caching</p>]]></content:encoded>
      <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Engineering</category>
    </item>
    <item>
      <title>The Impact of Words in Professional &amp; Personal Spaces</title>
      <link>https://insight.jatinjainsaraf.com/the-impact-of-words-in-professional-personal</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-impact-of-words-in-professional-personal</guid>
      <description>The Impact of Words in Professional &amp; Personal Spaces</description>
      <content:encoded><![CDATA[<h1>The Impact of Words in Professional &#x26; Personal Spaces</h1>
<p>The Impact of Words in Professional &#x26; Personal Spaces</p>
<p>Words have more weight than we realise,
especially when used as “jokes.”</p>
<p>A joke should make the person involved smile, not shrink.
But if someone feels stressed, low, or embarrassed because of it…</p>
<p>Then it’s not humour.
It’s harm wrapped in laughter.</p>
<p>And context matters more than we think:</p>
<p>Something said lightly between two people
can feel very different when repeated in a group.
The intention may stay the same,
but the impact can change completely.</p>
<p>In professional and personal spaces,
we often underestimate how far our words travel
and how deeply they can affect someone.</p>
<p>Choosing our words with awareness
isn’t just communication;
It’s leadership, respect, and humanity.</p>]]></content:encoded>
      <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Insights</category>
    </item>
    <item>
      <title>The Responsibility Behind Honest Feedback</title>
      <link>https://insight.jatinjainsaraf.com/leadership-wisdom-workplace-ethics-responsible-leadership</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/leadership-wisdom-workplace-ethics-responsible-leadership</guid>
      <description>The Responsibility Behind Honest Feedback</description>
      <content:encoded><![CDATA[<h1>The Responsibility Behind Honest Feedback</h1>
<p>🌱 The Responsibility Behind Honest Feedback</p>
<p>Feedback is often spoken about as a tool for improvement.
But rarely do we talk about the responsibility that comes with giving it.</p>
<p>A performance review isn’t just a formality.
It carries weight, real career, emotional, and developmental weight.
A single written sentence has the power to uplift someone, redirect them, or hold up an uncomfortable mirror they’ve avoided for too long.</p>
<p>That’s why honest feedback must be delivered with two things:
integrity and intention.</p>
<p>Integrity: because hiding the truth helps no one.
Intention: because the purpose is growth, not punishment.</p>
<p>Sugarcoating delays development.
Harshness without clarity destroys confidence.
But constructive, fact-based feedback, even when difficult, is an investment in someone’s future ability.</p>
<p>The real challenge isn’t writing the review.
It’s carrying the awareness that those words influence:
•	someone’s career trajectory
•	someone’s self-belief
•	someone’s next year of effort
•	and sometimes, someone’s livelihood</p>
<p>This is why feedback needs to be grounded, specific, accountable, and compassionate, not emotional, exaggerated, or careless.</p>
<p>When influence is handled responsibly, it guides.
When it isn’t, it wounds.</p>
<p>In every organization, feedback isn’t just an HR requirement.
It’s a leadership moment,
one that quietly shapes people long after the meeting ends.</p>
<p>#LeadershipWisdom
#WorkplaceEthics
#ResponsibleLeadership
#ProfessionalGrowth</p>]]></content:encoded>
      <pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Insights</category>
      <category>Leadership</category>
    </item>
    <item>
      <title>Your Blockchain Indexer Is Fine — Until It Isn’t</title>
      <link>https://insight.jatinjainsaraf.com/your-blockchain-indexer-is-fine-until-it-isnt</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/your-blockchain-indexer-is-fine-until-it-isnt</guid>
      <description>What nobody tells you about hitting 10 TB of indexed blockchain data, and why “just optimize it later” is the most expensive sentence in this industry.</description>
      <content:encoded><![CDATA[<h1>Your Blockchain Indexer Is Fine — Until It Isn’t</h1>
<p>What nobody tells you about hitting 10 TB of indexed blockchain data, and why “just optimize it later” is the most expensive sentence in…</p>
<hr>
<h3>Your Blockchain Indexer Is Fine — Until It Isn’t</h3>
<blockquote>
<p>What nobody tells you about hitting 10 TB of indexed blockchain data, and why “just optimize it later” is the most expensive sentence in this industry.</p>
</blockquote>
<p>The first year of building a blockchain indexer is deceptively smooth.</p>
<p>Blocks arrive. Transactions get parsed. Wallets accumulate state. Your PostgreSQL instance hums along. Queries return in milliseconds. Storage is cheap. The team ships features.</p>
<p>Then year two happens. Then year three.</p>
<p>Somewhere between 1 TB and 5 TB, your infrastructure bill starts a quiet conversation with your CTO. By 10 TB, which a high-throughput chain reaches within a few years of production indexing, that conversation gets loud and you are no longer talking about features. You are talking about survival: how do you change anything in a live system of this size without taking it down, corrupting data, or spending six months on a migration that should have taken three weeks?</p>
<p>This is that article. Not the “here’s how to build an indexer” tutorial. The one about what happens <em>after</em> — when the data is already there, it’s already the wrong shape, and the chain doesn’t stop producing blocks while you figure it out.</p>
<h3>What Makes Move Different at the Data Layer</h3>
<p>Before talking about scale, it’s worth understanding why Move-based blockchains (Aptos, Sui, Supra, and others) create a fundamentally different indexing problem than EVM chains.</p>
<p><strong>EVM state is shallow.</strong> An Ethereum indexer primarily deals with: blocks, transactions, logs, and account balances. The schema is wide but not deep. Relationships are relatively flat.</p>
<p><strong>Move state is deep and typed.</strong> Move organises on-chain state as <em>typed resources</em> attached to addresses. A single wallet doesn’t just have a balance — it can own dozens of distinct resource types simultaneously: coin stores, object collections, automation registrations, governance positions, staking records. Each resource type has its own structure. Each needs to be indexed, tracked, and queryable.</p>
<p><strong>Move has a first-class object model.</strong> In chains like Sui and Supra, everything is an addressable object with an owner, a version, a type path, and a parent-child relationship to other objects. Your indexer isn’t just recording what happened — it’s maintaining a live, queryable snapshot of an entire object graph that mutates with every block.</p>
<p><strong>Events are strongly typed.</strong> Move’s event system emits structured, typed payloads — not raw byte arrays like EVM logs. This is powerful for developers querying the indexer, but it means your event storage needs to handle variable schemas across dozens of event types, each with different fields, all arriving in the same stream.</p>
<p>The consequence of all this: a production Move indexer has <strong>far more tables, far more relationships, and far more cross-references</strong> than an EVM equivalent. A realistic production schema has 35–50 tables. Most of them are interconnected. Most of them grow with every block.</p>
<p>That scale of interconnection is exactly what makes optimisation so painful later.</p>
<h3>A Note Before We Go Further</h3>
<p>The patterns described in this article were not born from carelessness — they were the sensible choices given what was known at the time. The purpose of laying them out here is not to assign blame, but to share the experience of running into them at scale, so engineers building similar systems today can recognize them early and make the tradeoffs consciously rather than discover the cost years later.</p>
<h3>The Compounding Problem: Why Early Decisions Are Permanent</h3>
<p>Here is the core tension of blockchain indexer design: <strong>you make your schema decisions when you have zero data, but you live with those decisions when you have 10 TB of it.</strong></p>
<p>Every structural inefficiency you introduce on day one doesn’t just cost you N bytes per row. It costs you N bytes multiplied by every row ever written, across every table that inherited the pattern, indexed twice over because PostgreSQL also stores your mistakes in its B-tree indexes.</p>
<p>Let’s make this concrete.</p>
<h3>The Repeated String Problem</h3>
<p>Imagine you store a short identifier — say, the name of the network environment (“mainnet”, “testnet”) — as a <code>VARCHAR</code> column in every table. Seems harmless. It's just a word.</p>
<p>Now imagine that string is present in 40 tables. In every row of every one of those tables. At 10 billion total rows distributed across those tables — entirely realistic after three years on a fast chain — that one string, repeated, costs you <strong>tens to hundreds of gigabytes</strong> depending on its length.</p>
<p>But here’s where it gets worse: you can’t just <code>ALTER TABLE</code> your way out of this at 10 TB. An <code>ALTER TABLE</code> that changes a column type on a 2-billion-row table doesn't complete in minutes. It locks the table. It rewrites the heap. It takes hours or days. And you have 40 tables to fix. And the chain is still producing blocks.</p>
<p>The “right” solution — replacing the repeated string with a 2-byte integer foreign key — would have cost 30 minutes on day one. At year three, it costs you a multi-month migration project with significant operational risk.</p>
<h3>The Fat Primary Key Problem</h3>
<p>A transaction hash is typically 66 characters. A timestamp is 8 bytes. If your transaction table’s primary key is a composite of these plus an environment name, you’ve created a <strong>~90-byte primary key</strong>.</p>
<p>That primary key gets copied — physically, on disk — into every foreign key column of every child table. A transaction-heavy Move indexer has 10 or more child tables per transaction: events, state changes, coin transfers, fungible asset movements, sender records, fee payer records, and payload details. Each one stores that full 90-byte reference on every row.</p>
<p>At 10 billion transactions with 8 child rows each on average:</p>
<p>90 bytes × 10 child tables × 8 rows × 10B transactions<br>
= ~72 TB of foreign key storage alone</p>
<p>And PostgreSQL builds a B-tree index on every foreign key. That doubles it.</p>
<p>A 4-byte or 8-byte surrogate integer primary key — decided on day one — reduces this by a factor of 10 to 20. But retrofitting surrogate keys onto live tables with hundreds of billions of rows, active foreign key constraints, and applications reading those columns in real time is one of the hardest classes of database migrations that exists.</p>
<h3>The Wrong Type Problem</h3>
<p>Move’s RPC layer returns numeric values as strings because they can exceed JavaScript’s safe integer range. <code>blockHeight: "18000000"</code>. <code>sequenceNumber: "4294967298"</code>. <code>version: "999999999"</code>.</p>
<p>Indexer developers, working fast, copy this directly to the database: <code>blockheight VARCHAR(256)</code>. <code>version VARCHAR(10)</code>. <code>sequencenumber VARCHAR(255)</code>.</p>
<p>It works. For a while.</p>
<p>Then someone tries to write <code>WHERE blockheight BETWEEN 15000000 AND 16000000</code>. Postgres does lexicographic comparison on VARCHAR. The query returns wrong results. A bug is filed. A cast is added. The cast prevents index use. The query slows down. An index is added on <code>CAST(blockheight AS BIGINT)</code>. Now you have a functional index instead of a simple column index, which is harder to maintain and slower to build.</p>
<p>At 2 billion blocks, changing <code>VARCHAR(256)</code> to <code>BIGINT</code> requires a full table rewrite. At 10 TB, that's a weekend project, not a 10-minute fix.</p>
<h3>JSONB: The Tool That Becomes the Problem</h3>
<p>JSONB is genuinely useful for a Move indexer. Move resources have variable schemas. Event payloads differ by type. Storing semi-structured data in a relational database without JSONB means either a hundred narrow tables or an unmaintainable EAV design.</p>
<p>But JSONB has a cost that surprises people who haven’t run it at scale.</p>
<p><strong>Storage overhead is fixed regardless of content.</strong> A JSONB column storing <code>{}</code> still has ~20–30 bytes of binary representation overhead. When you have millions of rows with small or empty JSON blobs, that overhead dominates.</p>
<p><strong>Updates are expensive.</strong> PostgreSQL stores JSONB as an immutable value. When you update a JSONB column — even to change a single key — the database writes a completely new version of the entire document and marks the old one dead. For tables that update JSONB columns frequently (like wallet state, which changes on every relevant transaction), this creates significant table bloat over time. The dead tuples accumulate. VACUUM can’t always keep up. The table grows even if the actual data size doesn’t.</p>
<p><strong>GIN indexes on JSONB are large and slow to build.</strong> A GIN index on a full JSONB column is many times larger than a B-tree index on a scalar column. On a table with billions of rows, building or rebuilding a GIN index is a multi-hour operation that consumes significant I/O.</p>
<p>The failure mode at scale is not that JSONB stops working. It’s that it gradually degrades — updates get slower, VACUUM runs longer, storage grows faster than the data warrants — and diagnosing the root cause in a 10 TB database with dozens of tables requires significant forensic work.</p>
<h3>The Migration Problem Is the Real Problem</h3>
<p>Everything described above has a known correct solution. Surrogate keys. Proper numeric types. Selective JSONB use. Hash storage as binary instead of hex strings. These are not novel ideas.</p>
<p>The reason they matter — the reason this article exists — is not the storage cost in isolation. It’s what that storage cost means when you try to fix it three years into production.</p>
<p><strong>You cannot take a blockchain indexer offline to migrate it.</strong> The chain does not pause. Blocks arrive every second. If your indexer falls behind, you create a gap in your data that is either expensive to backfill or permanently lost, depending on your node’s pruning settings.</p>
<p><strong>Schema migrations at this scale require orchestration, not just SQL.</strong> You cannot <code>ALTER TABLE indexer_transaction ADD COLUMN transaction_id BIGSERIAL</code> on a 3-billion-row table and move on with your day. You need to: add the column with no constraint, backfill in batches during low-traffic windows, add the constraint as <code>NOT VALID</code>, validate it in a separate step, update all application code to write to both old and new columns during the transition, cut over reads, then drop the old column. Each step has rollback implications. Each step must be tested against a full-size copy of production data.</p>
<p><strong>Foreign key changes require cascade planning.</strong> If you want to replace a composite FK with a surrogate integer FK, you need to: add the surrogate column to the parent, backfill it, add it to all child tables, backfill it there, drop the old FK constraints, add new ones, drop the old columns. For 10 child tables, each with billions of rows, this is months of careful work — not because any individual step is hard, but because the sequence must be correct and live traffic must be handled at every stage.</p>
<p><strong>Indexes cannot be rebuilt without significant I/O impact.</strong> Replacing a large text-based index with a smaller integer-based index on a table that’s being actively written requires <code>CREATE INDEX CONCURRENTLY</code> — which takes the load without locking, but takes proportionally longer and puts sustained read pressure on the disk. On a busy 10 TB database, a single large index rebuild can take 12–24 hours.</p>
<p>This is the real complexity of running a production Move indexer at scale. Not any single technical problem, but the compounding interaction of many small early decisions that each seemed reasonable at the time — and the enormous operational cost of unwinding them under load, without downtime, while the chain keeps running.</p>
<h3>What Good Design Buys You</h3>
<p>The flip side of all this is worth stating clearly.</p>
<p>A team that makes the right schema decisions at the start — surrogate integer keys, correct numeric types, binary hash storage, selective JSONB use, properly normalised repeated strings — doesn’t just save storage. They buy themselves <em>optionality</em>.</p>
<p>At 10 TB with a well-designed schema, adding a new index takes hours, not days. Migrating a table to a new structure is a weekend project, not a quarter-long initiative. Backfilling new columns is a batch job, not a rewrite. Query optimisation is about access patterns, not fighting the type system.</p>
<p>The teams that build Move indexers correctly from the start can spend year three shipping new features and supporting developers building on their data. The teams that don’t spend year three fighting their own database.</p>
<p>That’s the real cost of getting it wrong. Not the terabytes. The lost time.</p>
<h3>Closing Thought</h3>
<p>If you are designing a Move blockchain indexer today — or evaluating one — the questions worth asking are not just “does it index all transaction types?” or “what’s the query latency?” They are:</p>
<ul>
<li>What does the schema look like at 100× current data volume?</li>
<li>How long does it take to add a column to your largest table?</li>
<li>Can you change a primary key structure without taking the system down?</li>
<li>How do you handle schema migrations under continuous write load?</li>
</ul>
<p>The answers reveal a lot about how much technical debt is already priced into the system — and how much of your future engineering capacity it will consume.</p>
<p>Blockchain data is permanent and ever-growing. Your schema decisions are not quite permanent — but at 10 TB, they’re close enough that the difference barely matters.</p>
<p>The Move ecosystem is still early, and the tooling for large-scale indexer operations is still maturing. If you are building in this space and want to compare notes, the comments are open.</p>
<p>By Jatin Jain Saraf on May 5, 2026.</p>]]></content:encoded>
      <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Web3</category>
      <category>Blockchain</category>
    </item>
    <item>
      <title>Database Efficiency 101: Understanding Bloat, Vacuum, and the Power of pg_repack</title>
      <link>https://insight.jatinjainsaraf.com/database-efficiency-101-understanding-bloat-vacuum-and-the-power-of-pg_repack</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/database-efficiency-101-understanding-bloat-vacuum-and-the-power-of-pg_repack</guid>
      <description>In a high-performance database environment, how we manage storage is just as important as how we write our queries.</description>
      <content:encoded><![CDATA[<h1>Database Efficiency 101: Understanding Bloat, Vacuum, and the Power of pg_repack</h1>
<p>In a high-performance database environment, how we manage storage is just as important as how we write our queries. To keep our systems…</p>
<hr>
<h3><strong>Database Efficiency 101: Understanding Bloat, Vacuum, and the Power of pg_repack</strong></h3>
<p>In a high-performance database environment, how we manage storage is just as important as how we write our queries. To keep our systems fast and cost-effective, we need to address a natural phenomenon in PostgreSQL known as <strong>Bloat</strong>.</p>
<h3>1. What is Database Bloat?</h3>
<p>PostgreSQL uses a system called <strong>MVCC</strong> (Multi-Version Concurrency Control). When a row is updated, Postgres doesn’t overwrite the old data; it creates a new version of the row and marks the old one as “dead.” Similarly, deleted rows are merely marked as dead tuples.</p>
<p>These dead tuples are “ghosts” — they occupy physical space on the disk but are invisible to your application. <strong>Bloat</strong> is the accumulation of these dead tuples within your tables and indexes.</p>
<p>— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —</p>
<h3>2. The Maintenance Arsenal</h3>
<h4>The Autovacuum (Your Internal Autopilot)</h4>
<p>The <strong>Autovacuum</strong> daemon is a background process that automatically cleans up dead tuples so the space can be reused. To keep it efficient, we focus on three main settings:</p>
<ul>
<li><strong>Scale Factor (</strong><code>**autovacuum_vacuum_scale_factor**</code><strong>)</strong>: This determines the threshold for when a vacuum starts. For example, a 20% scale factor means 20% of a table must be "dead" before a cleanup triggers.</li>
<li><strong>Cost Delay (</strong><code>**autovacuum_vacuum_cost_delay**</code><strong>)</strong>: This tells the worker to "pause" occasionally to ensure it doesn't consume all your Disk I/O, protecting application performance.</li>
<li><strong>Workers</strong>: This is the number of parallel processes allowed to clean the database at once.</li>
</ul>
<h4>VACUUM FULL (The Heavy Reset)</h4>
<p><code>VACUUM FULL</code> physically rewrites a table to a new file, reclaiming 100% of the bloat and returning that space to the Operating System.</p>
<ul>
<li><strong>The Problem</strong>: It requires an <strong>Access Exclusive Lock</strong>, meaning no one can read or write to the table while it’s running. In a 24/7 environment, this is rarely an option.</li>
</ul>
<h4>pg_repack (The Professional Solution)</h4>
<p><strong>pg_repack</strong> offers the best of both worlds. It reclaims 100% of the bloat and returns space to the OS, but it does so <strong>online</strong>.</p>
<ul>
<li><strong>How it works</strong>: It creates a new “clean” version of the table in the background, logs new changes, and then swaps them in a fraction of a second at the very end.</li>
</ul>
<p>— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —</p>
<h3>3. Why is Bloat Removal Necessary?</h3>
<ul>
<li><strong>Faster Querying</strong>: Bloat forces the database to read through “empty” space during scans. Removing bloat reduces Disk I/O and speeds up response times.</li>
<li><strong>Cost Optimization</strong>: We pay for the storage we provision. Our audit identified over <strong>620 GB</strong> of recoverable “ghost” space. Reclaiming this reduces our infrastructure spend.</li>
<li><strong>System Stability</strong>: Excessive bloat makes indexes larger and slower to update, eventually leading to performance “jitter” during peak traffic.</li>
</ul>
<p>— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —</p>
<h3>4. Our Execution Strategy</h3>
<p>To minimize risk, we process the database using a <strong>Sequential Repack Strategy</strong>. Instead of a massive operation, we process one table at a time. This ensures that the maximum additional storage required is only the size of the largest single table being processed, rather than a doubling of our entire database footprint.</p>
<p>By clearing out the <strong>90%+ bloat</strong> found in our smaller tables first, we create a “buffer” of free space to safely handle our largest multi-terabyte partitions.</p>
<p>By Jatin Jain Saraf on March 13, 2026.</p>]]></content:encoded>
      <pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>PostgreSQL</category>
    </item>
    <item>
      <title>The Power of LATERAL Joins: Turning SQL into a “ForEach” Loop</title>
      <link>https://insight.jatinjainsaraf.com/the-power-of-lateral-joins-turning-sql-into-a-foreach-loop</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-power-of-lateral-joins-turning-sql-into-a-foreach-loop</guid>
      <description>In the world of SQL, we are taught to think in sets. We join Table A to Table B based on a common key, and the database engine decides</description>
      <content:encoded><![CDATA[<h1>The Power of LATERAL Joins: Turning SQL into a "ForEach" Loop</h1>
<p>Most developers exclusively use an ORM — Prisma, Sequelize, Hibernate — which means they may never actually learn how to "loop" in SQL. For 90% of scenarios, that's fine. But when you hit a table with 10M+ rows and your ORM-generated join is spilling to disk, you need to understand what's actually happening underneath.</p>
<p>LATERAL joins are the SQL construct that turns a set-based operation into something that behaves like a <code>forEach</code> loop. Once you understand it, a whole class of slow queries becomes fast.</p>
<h2>What Is a LATERAL Join?</h2>
<p>In a standard SQL join, the subquery in your <code>FROM</code> clause is evaluated once, independently of the outer query. It cannot reference columns from tables listed before it.</p>
<p>A <code>LATERAL</code> subquery breaks that rule. It's re-evaluated <strong>for each row</strong> of the preceding table, and it <strong>can reference columns from that row</strong>. That's the entire mental model:</p>
<blockquote>
<p><strong>lateral = per-row subquery execution</strong></p>
</blockquote>
<pre><code class="language-sql">SELECT *
FROM outer_table o,
LATERAL (
  SELECT *
  FROM inner_table i
  WHERE i.foreign_key = o.id   -- references the current outer row
  LIMIT 3
) sub;
</code></pre>
<p>This is semantically equivalent to a <code>forEach</code> in application code:</p>
<pre><code class="language-javascript">for (const outerRow of outerTable) {
  const results = innerTable
    .filter(r => r.foreign_key === outerRow.id)
    .slice(0, 3);
}
</code></pre>
<p>The difference: it happens entirely in the database, in a single query, with the planner able to use indexes on every iteration.</p>
<h2>Use Case 1: Top-N Per Group</h2>
<p>The most common LATERAL use case — fetch the N most recent rows for each parent record.</p>
<p><strong>Problem:</strong> You have a <code>wallets</code> table and a <code>transactions</code> table. You want the 3 most recent transactions for each wallet.</p>
<p>The naive approach with a window function:</p>
<pre><code class="language-sql">-- Works, but scans and ranks all transactions first
SELECT *
FROM (
  SELECT
    w.id AS wallet_id,
    t.*,
    ROW_NUMBER() OVER (
      PARTITION BY t.wallet_id
      ORDER BY t.created_at DESC
    ) AS rn
  FROM wallets w
  JOIN transactions t ON t.wallet_id = w.id
) ranked
WHERE rn &#x3C;= 3;
</code></pre>
<p>This has to read and rank <strong>every transaction</strong> before filtering. On 20M rows, that's a full table scan.</p>
<p><strong>With LATERAL:</strong></p>
<pre><code class="language-sql">SELECT w.id AS wallet_id, recent.*
FROM wallets w
CROSS JOIN LATERAL (
  SELECT *
  FROM transactions t
  WHERE t.wallet_id = w.id
  ORDER BY t.created_at DESC
  LIMIT 3
) recent;
</code></pre>
<p>With a composite index on <code>(wallet_id, created_at DESC)</code>, the planner uses a nested loop: for each wallet, it does an index seek to grab exactly 3 rows. No full scan. No ranking pass.</p>
<pre><code class="language-sql">CREATE INDEX idx_txn_wallet_created
  ON transactions (wallet_id, created_at DESC);
</code></pre>
<p>On 500K wallets with 20M transactions: the window function version takes 40+ seconds; the LATERAL version takes under 500ms.</p>
<h2>Use Case 2: Forcing Nested Loops — 29 Minutes to 0.3ms</h2>
<p>This is where LATERAL becomes a surgical tool for query optimization.</p>
<p>The planner makes join strategy decisions based on table statistics. When it gets it wrong — choosing a hash join or merge join on a large table — you can use LATERAL to force the strategy you know is correct.</p>
<p><strong>A real production example from SupraScan:</strong> We had a query joining <code>blocks</code> to <code>events</code> to find the latest event of each type for a given contract. The planner chose a merge join, sorting hundreds of thousands of rows.</p>
<pre><code class="language-sql">-- Before: planner chose merge join — 29 minutes
SELECT DISTINCT ON (e.event_type)
  e.*
FROM blocks b
JOIN events e ON e.block_height = b.height
WHERE b.chain_id = $1
  AND e.contract_address = $2
ORDER BY e.event_type, e.created_at DESC;
</code></pre>
<p><strong>After — LATERAL forces a targeted index seek per event type:</strong></p>
<pre><code class="language-sql">-- After: nested loop via LATERAL — 0.3ms
SELECT latest.*
FROM (
  SELECT UNNEST(ARRAY['Transfer', 'Mint', 'Burn', 'Approval']) AS event_type
) types
CROSS JOIN LATERAL (
  SELECT e.*
  FROM events e
  WHERE e.contract_address = $2
    AND e.event_type = types.event_type
  ORDER BY e.created_at DESC
  LIMIT 1
) latest;
</code></pre>
<pre><code class="language-sql">-- The index that makes each iteration a single seek
CREATE INDEX idx_events_contract_type_created
  ON events (contract_address, event_type, created_at DESC);
</code></pre>
<p>With this index, each LATERAL iteration is one index seek. Four event types = four index seeks, each returning exactly one row. The planner cannot choose anything other than nested loops — which is exactly what you want when your outer set is small and your index is selective.</p>
<p><strong>Result: 29 minutes → 0.3 milliseconds.</strong></p>
<p>The key insight is that LATERAL <strong>keeps your working set small at each step</strong>. Instead of joining two large tables and filtering afterward, you filter first — at the index level — before any join. Memory stays low, disk spills don't happen, and the index works on every iteration.</p>
<h2>Use Case 3: Unnesting JSONB and Arrays</h2>
<p>LATERAL joins are essential when working with set-returning functions like <code>UNNEST</code> and <code>jsonb_array_elements</code>, because those functions need per-row context from the outer table.</p>
<p><strong>Example — JSONB array expansion:</strong></p>
<pre><code class="language-sql">-- users table with a preferences JSONB column containing an array
-- Expand each preference entry and filter enabled ones

SELECT u.id, u.email, pref.value
FROM users u
CROSS JOIN LATERAL jsonb_array_elements(u.preferences) AS pref(value)
WHERE pref.value->>'enabled' = 'true';
</code></pre>
<p>Without LATERAL, you cannot reference <code>u.preferences</code> inside <code>jsonb_array_elements</code>. The implicit comma syntax (<code>,</code> instead of <code>CROSS JOIN LATERAL</code>) works too — PostgreSQL treats it as LATERAL when a set-returning function appears — but explicit <code>LATERAL</code> makes the intent clear.</p>
<p><strong>Example — plain array unnesting with a join:</strong></p>
<pre><code class="language-sql">-- posts table has a tags integer array; join each tag to a tag_definitions table
SELECT p.id, p.title, t.name AS tag_name
FROM posts p
CROSS JOIN LATERAL UNNEST(p.tag_ids) AS tag_id
JOIN tag_definitions t ON t.id = tag_id;
</code></pre>
<p>This pattern replaces multiple round-trips or suboptimal <code>ANY(array)</code> queries with a single clean pass.</p>
<h2>Use Case 4: Keyset Pagination</h2>
<p>Standard <code>OFFSET</code> pagination breaks on large tables — scanning 500K rows to skip the first 499,990 is a full table read disguised as a feature.</p>
<p>LATERAL enables efficient cursor-based pagination:</p>
<pre><code class="language-sql">-- Get the next 20 transactions per wallet after a given cursor
SELECT w.id, page.*
FROM wallets w
CROSS JOIN LATERAL (
  SELECT id, amount, created_at
  FROM transactions t
  WHERE t.wallet_id = w.id
    AND (t.created_at, t.id) &#x3C; ($last_seen_at::timestamptz, $last_seen_id::bigint)
  ORDER BY t.created_at DESC, t.id DESC
  LIMIT 20
) page
WHERE w.user_id = $user_id;
</code></pre>
<p>Page 500 loads in the same time as page 1. The <code>(created_at, id)</code> tuple comparison is the cursor — the index makes each seek <code>O(log n)</code> regardless of which page you're on.</p>
<h2>When NOT to Use LATERAL</h2>
<p>LATERAL is the right tool when:</p>
<ul>
<li>Your outer set is <strong>small</strong> and the inner query is <strong>highly selective</strong> (with a matching index)</li>
<li>You need top-N per group</li>
<li>You're working with set-returning functions (<code>UNNEST</code>, <code>jsonb_array_elements</code>)</li>
<li>You need to override the planner's join strategy choice</li>
</ul>
<p><strong>Don't reach for LATERAL when:</strong></p>
<ul>
<li>You're doing a simple 1:1 join — regular <code>JOIN</code> optimizes better</li>
<li>Your outer set is large and the inner query isn't selective — you'll get N expensive seeks instead of one efficient scan</li>
<li>The planner's chosen strategy is actually correct for your data distribution</li>
</ul>
<p>Always verify with <code>EXPLAIN (ANALYZE, BUFFERS)</code>:</p>
<pre><code class="language-sql">EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT w.id, recent.*
FROM wallets w
CROSS JOIN LATERAL (
  SELECT * FROM transactions t
  WHERE t.wallet_id = w.id
  ORDER BY t.created_at DESC
  LIMIT 3
) recent;
</code></pre>
<p>Look for <code>Nested Loop</code> + <code>Index Scan</code> in the plan output. If you see <code>Hash Join</code> or <code>Seq Scan</code>, the index is missing or statistics are stale — run <code>ANALYZE transactions</code> and re-check.</p>
<h2>Summary</h2>





























<table><thead><tr><th>Pattern</th><th>When to use</th></tr></thead><tbody><tr><td>Top-N per group</td><td>Small outer set, large inner table, composite index available</td></tr><tr><td>Force nested loop</td><td>Planner chooses wrong strategy; outer set is small and selective</td></tr><tr><td>JSONB / array expansion</td><td>Any set-returning function that needs parent row context</td></tr><tr><td>Keyset pagination</td><td>Replace OFFSET on large tables</td></tr><tr><td>Avoid</td><td>1:1 joins, large outer sets without selective indexes</td></tr></tbody></table>
<p>A LATERAL join lets a subquery reference the current row of the outer query. Use it when you want the planner to perform N targeted index seeks instead of one large scan — and when you have the index to make each seek fast.</p>
<p>The moment your ORM-generated query starts spilling to disk on a large table, check whether LATERAL can replace it. Drop to raw SQL, write the LATERAL subquery, create the composite index, and run <code>EXPLAIN ANALYZE</code>. The plan will change from <code>Seq Scan → Hash Join → Sort</code> to <code>Nested Loop → Index Scan</code> — and your query time will drop with it.</p>
<hr>
<p><em>All examples tested on PostgreSQL 15+. The 29-minute → 0.3ms optimization is from a real production query on SupraScan's blockchain indexer, running on a PostgreSQL database that grew to 10 TB of indexed blockchain data.</em></p>]]></content:encoded>
      <pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>SQL</category>
      <category>PostgreSQL</category>
    </item>
    <item>
      <title>PostgreSQL 18: A Paradigm Shift for Modern Data Workloads</title>
      <link>https://insight.jatinjainsaraf.com/postgresql-18-a-paradigm-shift-for-modern-data-workloads</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/postgresql-18-a-paradigm-shift-for-modern-data-workloads</guid>
      <description>PostgreSQL has long been the gold standard for relational databases, robust, reliable, and continuously evolving. With each major release…</description>
      <content:encoded><![CDATA[<h1>PostgreSQL 18: A Paradigm Shift for Modern Data Workloads</h1>
<p>PostgreSQL has long been the gold standard for relational databases, robust, reliable, and continuously evolving. With each major release…</p>
<hr>
<h3>PostgreSQL 18: A Paradigm Shift for Modern Data Workloads</h3>
<p>PostgreSQL has long been the gold standard for relational databases, robust, reliable, and continuously evolving. With each major release, the community eagerly anticipates new features that enhance performance, developer experience, and operational efficiency. PostgreSQL 18, released in late 2025, isn’t just another incremental update; it’s a paradigm shift that fundamentally re-architects how the database handles I/O, optimises queries, and manages complex deployments.</p>
<p>For developers, DBAs, and architects, understanding the core changes in PG18 isn’t optional — it’s crucial for future-proofing your applications and infrastructure. Let’s dive deep into what makes PG18 a monumental leap from its predecessors, outlining the key features, their impact, and a balanced look at the pros and cons.</p>
<h3>The Evolution: A Quick Look Back from PG17 to PG18</h3>
<p>Before we dissect PG18, it’s helpful to understand the trajectory. Previous versions, including PG17, brought significant improvements like enhanced JSONB capabilities, better parallelism for certain queries, and continued refinements to the query planner. These were vital, but PG18 tackles some long-standing architectural bottlenecks.</p>
<p>The jump to PG18 is about addressing foundational performance limitations (I/O, index usage) and critical operational hurdles (schema replication, upgrade pain points).</p>
<h3>The Big Four: What Defines PostgreSQL 18</h3>
<p>While many smaller improvements arrived, four features stand out as the pillars of PostgreSQL 18:</p>
<h3>1. Asynchronous I/O (AIO) Subsystem: The End of Waiting</h3>
<p>What it is: In a radical departure, PostgreSQL 18 introduces a fully integrated Asynchronous I/O (AIO) subsystem. Previously, PostgreSQL primarily used synchronous I/O; when a process needed data from disk, it would issue a request to the operating system and then <em>wait</em> for the data to be read. This meant CPU cycles were wasted in idle waiting, especially on high-latency storage or during heavy read operations.</p>
<p>PG18 changes this by allowing the database to issue multiple I/O requests concurrently without blocking the initiating process. It leverages modern OS features like Linux’s <code>io_uring</code> for highly efficient I/O batching and completion.</p>
<p><strong>Comparison (PG17 vs. PG18):</strong></p>
<ul>
<li>PG17 &#x26; Earlier: CPU-bound by disk latency; processes often stalled waiting for I/O.</li>
<li>PG18: I/O-bound processes can now issue requests and continue processing other tasks, dramatically improving CPU utilisation and throughput.</li>
</ul>
<p><strong>Pros:</strong></p>
<ul>
<li>Massive Performance Boost: Up to 3x faster for read-heavy workloads (scans, joins, <code>VACUUM</code>).</li>
<li>Increased Concurrency: More transactions per second possible with the same hardware.</li>
<li>Smoother Operations: Background tasks like <code>VACUUM</code> can run with less impact on active queries.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>OS Dependence: Optimal performance leverages specific OS features (<code>io_uring</code> on Linux). While a fallback <code>io_method=worker</code> is available, the full benefit is OS-specific.</li>
<li>Configuration Nuances: Requires understanding new <code>io_method</code> parameters and potential OS-level tuning.</li>
</ul>
<h3>2. B-Tree Skip Scans: Smart Indexing for Complex Queries</h3>
<p>What it is: A common pain point with multicolumn B-Tree indexes (e.g., <code>(column_A, column_B, column_C)</code>) was their strict reliance on the "leftmost prefix rule." If your query filtered only on <code>column_B</code> or <code>column_C</code> without <code>column_A</code>, The index was often ignored, leading to slow sequential scans.</p>
<p>PG18 introduces B-Tree Skip Scans. The query planner can now intelligently “skip” over distinct values of leading index columns to efficiently locate data based on subsequent columns.</p>
<p><strong>Comparison (PG17 vs. PG18):</strong></p>
<ul>
<li>PG17 &#x26; Earlier: Often required to create multiple, redundant indexes (e.g., <code>(A,B,C)</code>, <code>(B,C)</code>, <code>(C)</code>) to optimise different query patterns, leading to index bloat and slower writes.</li>
<li>PG18: A single composite index can now satisfy a wider range of query conditions, significantly reducing index maintenance overhead.</li>
</ul>
<p><strong>Pros:</strong></p>
<ul>
<li>Reduced Index Bloat: Fewer indexes needed to cover diverse query patterns.</li>
<li>Improved Write Performance: Less overhead during inserts/updates/deletes due to fewer indexes to maintain.</li>
<li>Faster Ad-Hoc Queries: Analytics and exploratory queries benefit greatly.</li>
<li>Simplified Schema Design: Easier to manage indexes without sacrificing query speed.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Not a Silver Bullet: The performance gain depends on data distribution and selectivity; not every query will see a huge boost.</li>
<li>Planner Learning Curve: DBAs need to understand how the planner now utilizes these indexes to avoid over-indexing.</li>
</ul>
<h3>3. Logical Replication for DDL: Seamless Schema Evolution</h3>
<p>What it is: Logical replication has been a cornerstone for building highly available, geographically distributed, or multi-tenant PostgreSQL systems. However, a major limitation was its inability to automatically replicate DDL (Data Definition Language) changes like <code>CREATE TABLE</code>, <code>ALTER TABLE</code>, or <code>DROP INDEX</code>. DBAs had to manually synchronize schema changes across all replicas, a process prone to errors and downtime.</p>
<p>PG18 resolves this by natively integrating DDL replication into the logical replication mechanism. Schema changes are now automatically propagated to subscribers.</p>
<p><strong>Comparison (PG17 vs. PG18):</strong></p>
<ul>
<li>PG17 &#x26; Earlier: Manual DDL sync, often involving custom scripts, change management, and potential replication breaks.</li>
<li>PG18: Automatic, atomic DDL propagation, making multi-node schema changes significantly safer and simpler.</li>
</ul>
<p><strong>Pros:</strong></p>
<ul>
<li>True Zero-Downtime Migrations: Critical for large-scale, 24/7 applications.</li>
<li>Simplified Operations: Eliminates complex manual procedures for schema updates.</li>
<li>Improved Consistency: Reduces the risk of schema drift between publisher and subscribers.</li>
<li>Enhanced DR Capabilities: Easier to maintain consistent schemas across disaster recovery setups.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li>Careful Planning Still Needed: While automated, complex DDL changes can still impact application logic on the subscriber side.</li>
<li>Potential for Conflicts: Requires understanding how certain DDL operations might interact in specific replication topologies.</li>
</ul>
<h3>4. Preserved Optimizer Statistics on Major Upgrades: Hit the Ground Running</h3>
<p>What it is: Previously, when performing a major version upgrade using <code>pg_upgrade</code>, the query planner's statistical information was reset. This meant after an upgrade, your database would perform poorly until you ran a full <code>ANALYZE</code> (which could take hours for large databases), as the planner had no information to create efficient execution plans.</p>
<p>PG18 ensures that query planner statistics are preserved during <code>pg_upgrade</code>.</p>
<p><strong>Comparison (PG17 vs. PG18):</strong></p>
<ul>
<li>PG17 &#x26; Earlier: Post-upgrade “cold start” period with suboptimal query performance until <code>ANALYZE</code> completed.</li>
<li>PG18: Database performs optimally immediately after the upgrade, minimizing service degradation.</li>
</ul>
<p>Pros:</p>
<ul>
<li>Faster, Smoother Upgrades: Reduces post-upgrade tuning and performance issues.</li>
<li>Reduced Risk: Eliminates a major source of performance anxiety after major version bumps.</li>
<li>Improved DBA Experience: Less manual intervention required after an upgrade.</li>
</ul>
<p>Cons:</p>
<ul>
<li>No Functional Impact: This is a quality-of-life improvement for upgrades, not a new runtime feature.</li>
</ul>
<h3>Other Notable Mentions in PostgreSQL 18</h3>
<ul>
<li>Native UUIDv7 Support: Generates timestamp-ordered UUIDs, drastically improving B-Tree index performance for UUID primary keys by reducing fragmentation.</li>
<li>Enhanced <code>RETURNING</code> Clause (OLD and NEW): Allows developers to retrieve both the original (<code>OLD</code>) and modified (<code>NEW</code>) row states in <code>UPDATE</code> and <code>DELETE</code> statements, simplifying auditing and application logic.</li>
<li>Virtual Generated Columns by Default: Generated columns are now virtual (computed on read) by default, saving significant disk space and improving write performance (you can still specify <code>STORED</code> if needed).</li>
<li>OAuth 2.0 Authentication: Modern enterprise-grade authentication support, simplifying integration with SSO providers.</li>
<li>MD5 Password Deprecation: Continues the push towards more secure SCRAM-SHA-256 for password authentication, with clear warnings for MD5 usage.</li>
</ul>
<h3>Conclusion: Embracing the Future with PostgreSQL 18</h3>
<p>PostgreSQL 18 is a testament to the community’s commitment to pushing the boundaries of relational database technology. The architectural shifts in AIO and the intelligent index utilisation of Skip Scans are foundational changes that will impact performance across the board. Combined with critical operational improvements like Logical DDL Replication and preserved optimizer statistics, PG18 makes managing and scaling PostgreSQL easier and more robust than ever before.</p>
<p>For any organization or developer serious about performance, reliability, and future-proofing their data layer, migrating to PostgreSQL 18 isn’t just an option — it’s a strategic imperative. The benefits far outweigh the considerations, marking a significant milestone in PostgreSQL’s storied history.</p>
<p>By Jatin Jain Saraf on February 21, 2026.</p>]]></content:encoded>
      <pubDate>Sat, 21 Feb 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>SQL</category>
    </item>
    <item>
      <title>Cloud‑to‑Cloud Database Migration: A Practical Guide for Large PostgreSQL Systems</title>
      <link>https://insight.jatinjainsaraf.com/cloudtocloud-database-migration-a-practical-guide-for-large-postgresql-systems</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/cloudtocloud-database-migration-a-practical-guide-for-large-postgresql-systems</guid>
      <description>Migrating infrastructure from one cloud provider to another is already a complex process. But migrating a large, pr</description>
      <content:encoded><![CDATA[<h1>Cloud‑to‑Cloud Database Migration: A Practical Guide for Large PostgreSQL Systems</h1>
<p>Migrating infrastructure from one cloud provider to another is already a complex process. But migrating a large, production PostgreSQL…</p>
<hr>
<h3>Cloud‑to‑Cloud Database Migration: A Practical Guide for Large PostgreSQL Systems</h3>
<p>Migrating infrastructure from one cloud provider to another is already a complex process. But migrating a large, production PostgreSQL database, with schemas, constraints, triggers, partitions, indexes, extensions, and years of operational history, is an entirely different level of engineering.</p>
<p>This article breaks down the real‑world challenges, decisions, risks, and best practices involved in such migrations.</p>
<h3>Why Database Migration Is Uniquely Hard</h3>
<p>Applications are portable.</p>
<p>Containers can be rebuilt.</p>
<p>CI pipelines can be recreated.</p>
<p>But databases are:</p>
<ul>
<li>Stateful</li>
<li>Performance‑sensitive</li>
<li>Integrity‑critical</li>
<li>Operationally fragile</li>
</ul>
<p>A single wrong command can:</p>
<ul>
<li>Drop constraints</li>
<li>Disable triggers</li>
<li>Break partitions</li>
<li>Corrupt encodings</li>
<li>Destroy indexes</li>
<li>Or silently degrade performance</li>
</ul>
<p>Cloud database migration is not data transfer.</p>
<p>It is data engineering + systems engineering + risk management.</p>
<h3>Common Use Cases for Cloud‑to‑Cloud Migration</h3>
<p>Teams usually migrate because of:</p>
<ul>
<li>Cost optimization (AWS → GCP / GCP → DO)</li>
<li>Vendor lock‑in concerns</li>
<li>Regulatory or data‑residency requirements</li>
<li>Performance issues</li>
<li>Infrastructure consolidation</li>
<li>Reliability problems</li>
</ul>
<p>Each use case affects the migration strategy.</p>
<p>Example:</p>
<ul>
<li>Cost‑driven migration → minimize downtime</li>
<li>Compliance migration → prioritize auditability</li>
<li>Performance migration → re‑design indexes &#x26; partitions</li>
</ul>
<h3>What Actually Needs to Be Migrated (Beyond Tables)</h3>
<p>Most failures happen because teams think only about data rows. In reality you must migrate:</p>
<h4>1. Schemas</h4>
<ul>
<li>Namespaces</li>
<li>Ownership</li>
<li>Permissions</li>
</ul>
<h4>2. Tables</h4>
<ul>
<li>Data types</li>
<li>Defaults</li>
<li>NOT NULL constraints</li>
</ul>
<h4>3. Indexes</h4>
<ul>
<li>B‑Tree / GIN / GiST / BRIN</li>
<li>Partial indexes</li>
<li>Expression indexes</li>
</ul>
<h4>4. Constraints</h4>
<ul>
<li>Primary keys</li>
<li>Foreign keys</li>
<li>Unique constraints</li>
<li>Check constraints</li>
</ul>
<h4>5. Partitions</h4>
<ul>
<li>Range / list / hash partitions</li>
<li>Partition constraints</li>
<li>Parent‑child table relationships</li>
</ul>
<h4>6. Triggers</h4>
<ul>
<li>Audit triggers</li>
<li>Business logic triggers</li>
<li>Replication triggers</li>
</ul>
<h4>7. Functions &#x26; Procedures</h4>
<ul>
<li>PL/pgSQL</li>
<li>Extensions</li>
</ul>
<h4>8. Extensions</h4>
<ul>
<li>PostGIS</li>
<li>pgcrypto</li>
<li>uuid‑ossp</li>
<li>citext</li>
<li>TimescaleDB</li>
</ul>
<h4>9. Migration history</h4>
<ul>
<li>Flyway / Liquibase tables</li>
<li>Schema version tracking</li>
</ul>
<p>Missing any one of these can break your system in subtle ways.</p>
<h3>Strategic Decisions Before You Touch Any Command</h3>
<h4>1. Downtime vs Live Migration</h4>
<p>Options:</p>
<ul>
<li>Full downtime migration</li>
<li>Read‑only window</li>
<li>Dual‑write period</li>
<li>Logical replication</li>
</ul>
<p>Each increases complexity exponentially.</p>
<h4>2. Database Engine Parity</h4>
<p>Verify:</p>
<ul>
<li>PostgreSQL major version</li>
<li>Extensions availability</li>
<li>Default configs</li>
<li>Collation &#x26; locale</li>
</ul>
<p>Even small version mismatches can break indexes or functions.</p>
<h4>3. Data Volume</h4>
<h4>4. Security Model</h4>
<ul>
<li>Users</li>
<li>Roles</li>
<li>Grants</li>
<li>Secrets</li>
</ul>
<p>Cloud providers handle IAM very differently.</p>
<h3>pg_dump &#x26; pg_restore: Where Most Migrations Fail</h3>
<h4>Choosing the Correct Dump Format</h4>
<h3>Recommended Commands</h3>
<h4>Schema only</h4>
<p>pg_dump -Fc --schema-only -f schema.dump dbname</p>
<h4>Data only</h4>
<p>pg_dump -Fc --data-only -f data.dump dbname</p>
<h4>Full dump</h4>
<p>pg_dump -Fd -j 8 -f dumpdir dbname</p>
<h3>Restore Order (Critical)</h3>
<ol>
<li>Roles &#x26; users</li>
<li>Schemas</li>
<li>Extensions</li>
<li>Tables</li>
<li>Data</li>
<li>Indexes</li>
<li>Constraints</li>
<li>Triggers</li>
<li>Validation</li>
</ol>
<p>Wrong order = broken foreign keys or missing triggers.</p>
<h3>The Dangerous Commands (Real Production Killers)</h3>
<p>pg_dumpall > backup.sql   # huge, slow, unsafe</p>
<p>pg_restore --clean        # may DROP critical objects</p>
<p>DROP SCHEMA public CASCADE;</p>
<p>SET session_replication_role = replica;  # disables triggers</p>
<p>One wrong flag → irreversible damage.</p>
<h3>Configuration Differences That Break Systems</h3>
<p>Cloud providers ship different defaults:</p>
<ul>
<li>work_mem</li>
<li>shared_buffers</li>
<li>wal_level</li>
<li>max_connections</li>
<li>autovacuum settings</li>
<li>synchronous_commit</li>
</ul>
<p>These affect:</p>
<ul>
<li>Query latency</li>
<li>Index usage</li>
<li>Deadlocks</li>
<li>Replication stability</li>
</ul>
<p>Always compare:</p>
<p>SHOW ALL;</p>
<h3>Performance Regression After Migration</h3>
<p>Common causes:</p>
<ul>
<li>Missing indexes</li>
<li>Statistics not refreshed</li>
<li>Different query planner</li>
<li>Changed collation</li>
<li>IO throughput differences</li>
</ul>
<p>Fix with:</p>
<p>ANALYZE;<br>
REINDEX DATABASE dbname;</p>
<h3>Validation Checklist (Non‑Negotiable)</h3>
<h4>Structure</h4>
<p>SELECT count(*) FROM information_schema.tables;</p>
<h4>Triggers</h4>
<p>SELECT * FROM information_schema.triggers;</p>
<h4>Indexes</h4>
<p>SELECT * FROM pg_indexes;</p>
<h4>Extensions</h4>
<p>SELECT * FROM pg_extension;</p>
<h4>Row counts</h4>
<p>Compare source vs target.</p>
<h4>Application tests</h4>
<p>Run full regression tests.</p>
<h3>Real Risks Nobody Talks About</h3>
<ul>
<li>Silent data truncation</li>
<li>Encoding mismatch</li>
<li>Timezone corruption</li>
<li>Floating‑point drift</li>
<li>Broken audit trails</li>
<li>Lost permissions</li>
</ul>
<p>Most of these are detected weeks later in production.</p>
<h3>When Your Database Is in Terabytes and Billions of Rows</h3>
<p>At small scale, mistakes are painful.</p>
<p>At terabyte scale, mistakes are catastrophic.</p>
<p>When your database contains:</p>
<ul>
<li>Multiple TB of data</li>
<li>Billions of rows</li>
<li>Hundreds of tables</li>
<li>Thousands of indexes</li>
<li>Complex partitions</li>
<li>Heavy write traffic</li>
</ul>
<p>…migration becomes a distributed systems problem, not just a database task.</p>
<h3>Additional Challenges at TB Scale</h3>
<h4>1. Network Throughput Becomes the Bottleneck</h4>
<ul>
<li>Cross-cloud bandwidth limits</li>
<li>Packet loss</li>
<li>Throttling</li>
<li>NAT saturation</li>
</ul>
<p>Even at 1 Gbps:</p>
<p>1 TB ≈ 2.5 hours (ideal conditions)</p>
<p>5 TB ≈ 12+ hours</p>
<p>10 TB ≈ 24+ hours</p>
<p>Real-world speeds are usually 40–60% of theoretical.</p>
<h4>2. Storage IOPS Decide Success or Failure</h4>
<p>Cloud disks differ massively:</p>
<ul>
<li>GCP PD-SSD</li>
<li>AWS gp3 / io2</li>
<li>DigitalOcean volumes</li>
</ul>
<p>Low IOPS =</p>
<ul>
<li>Slow restores</li>
<li>Checkpoint storms</li>
<li>Autovacuum backlog</li>
<li>Index build failures</li>
</ul>
<p>Always benchmark disk write speed before restore.</p>
<h4>3. pg_dump Stops Being Enough</h4>
<p>For very large databases:</p>
<ul>
<li>pg_dump becomes slow</li>
<li>Single-threaded metadata phase blocks</li>
<li>Restore time explodes</li>
</ul>
<p>Better approaches:</p>
<ul>
<li>pg_dump in directory format with high parallelism</li>
<li>Logical replication</li>
<li>Chunked table migration</li>
<li>Physical replication (pg_basebackup)</li>
</ul>
<h3>Migration Strategies for Massive Databases</h3>
<h4>Strategy 1: Full Downtime (Rarely Acceptable)</h4>
<ul>
<li>Stop writes</li>
<li>Dump everything</li>
<li>Restore</li>
<li>Start application</li>
</ul>
<p>Works only for internal systems or very tolerant products.</p>
<h4>Strategy 2: Schema First + Batch Data Migration</h4>
<ol>
<li>Dump schema</li>
<li>Create structure on target</li>
<li>Migrate tables in chunks</li>
<li>Build indexes later</li>
</ol>
<p>Reduces risk but complex to orchestrate.</p>
<h4>Strategy 3: Logical Replication (Most Common)</h4>
<ul>
<li>Setup publisher on source</li>
<li>Setup subscriber on target</li>
<li>Sync initial snapshot</li>
<li>Catch up changes</li>
<li>Switch traffic</li>
</ul>
<p>Pros:</p>
<ul>
<li>Near zero downtime</li>
<li>Safer cutover</li>
</ul>
<p>Cons:</p>
<ul>
<li>Complex</li>
<li>Requires identical schema</li>
<li>Triggers behave differently</li>
</ul>
<h4>Strategy 4: Physical Replication</h4>
<ul>
<li>Use pg_basebackup</li>
<li>Copy WAL files</li>
<li>Promote replica</li>
</ul>
<p>Fastest but risky across clouds and versions.</p>
<h4>The “One Wrong Command” Problem (At Scale)</h4>
<p>At TB scale, mistakes are not easily reversible.</p>
<p>Examples:</p>
<p>pg_restore --clean</p>
<p>→ Drops objects while application is live.</p>
<p>pg_restore -j 32</p>
<p>→ Can overload disks and crash the server.</p>
<p>pg_dump --disable-triggers</p>
<p>→ Breaks audit systems.</p>
<p>ALTER SYSTEM SET synchronous_commit = off;</p>
<p>→ Silent data loss during crash.</p>
<p>SET maintenance_work_mem = '64MB';</p>
<p>→ Index creation takes days instead of hours.</p>
<h3>Configuration That Can Break Large Restores</h3>
<p>Critical settings during restore:</p>
<p>maintenance_work_mem<br>
work_mem<br>
shared_buffers<br>
wal_buffers<br>
checkpoint_timeout<br>
max_wal_size<br>
synchronous_commit<br>
fsync<br>
autovacuum</p>
<p>Bad tuning leads to:</p>
<ul>
<li>WAL explosion</li>
<li>Disk full</li>
<li>Checkpoint storms</li>
<li>Table corruption</li>
<li>Restore freezing at 99%</li>
</ul>
<h3>pg_dump Format Decision Matrix (TB Scale)</h3>
<p>Recommended:</p>
<p>pg_dump -Fd -j 16 --no-owner --no-acl -f dumpdir dbname</p>
<p>Restore:</p>
<p>pg_restore -Fd -j 16 -d target_db dumpdir</p>
<h3>Data Integrity Verification at Massive Scale</h3>
<p>Row counts are not enough.</p>
<p>Also verify:</p>
<ul>
<li>Checksums</li>
<li>Hash totals</li>
<li>Min/max timestamps</li>
<li>Foreign key violations</li>
<li>Application-level aggregates</li>
</ul>
<p>Example:</p>
<p>SELECT SUM(amount) FROM transactions;</p>
<p>Compare source vs target.</p>
<h3>Operational Reality</h3>
<p>Large migrations require:</p>
<ul>
<li>Dry runs</li>
<li>Rollback plan</li>
<li>Monitoring dashboards</li>
<li>Disk usage alerts</li>
<li>WAL growth alerts</li>
<li>CPU saturation alerts</li>
</ul>
<p>And one truth:</p>
<p>You never migrate once.</p>
<p>You rehearse many times.</p>
<h3>Architecture Diagrams (Reference Patterns)</h3>
<p>Below are practical reference architectures commonly used for large-scale PostgreSQL migrations.</p>
<h4>1. Simple Offline Migration (Full Downtime)</h4>
<p>┌──────────────┐        pg_dump / pg_restore        ┌──────────────┐<br>
│  Source DB   │  ─────────────────────────────▶  │  Target DB   │<br>
│ (Cloud A)    │                                   │ (Cloud B)    │<br>
└──────────────┘                                   └──────────────┘<br>
▲                                                   ▲<br>
│                                                   │<br>
└────────────── Application Down ──────────────────┘</p>
<p>Use when:</p>
<ul>
<li>Internal tools</li>
<li>Small datasets</li>
<li>Downtime acceptable</li>
</ul>
<h4>2. Schema-First + Batch Data Migration</h4>
<p>Step 1: Schema<br>
┌──────────────┐   pg_dump --schema-only   ┌──────────────┐<br>
│  Source DB   │ ───────────────────────▶ │  Target DB   │<br>
└──────────────┘                           └──────────────┘</p>
<pre><code>      Step 2: Data (in chunks)  
</code></pre>
<p>┌──────────────┐   Table batches / jobs    ┌──────────────┐<br>
│  Source DB   │ ───────────────────────▶ │  Target DB   │<br>
└──────────────┘                           └──────────────┘</p>
<pre><code>      Step 3: Indexes + Triggers
</code></pre>
<p>Use when:</p>
<ul>
<li>Medium–large DB</li>
<li>Partial downtime allowed</li>
<li>Need more control</li>
</ul>
<h4>3. Logical Replication (Near Zero Downtime)</h4>
<p>Initial Snapshot + WAL Streaming<br>
┌──────────────┐  ───────────────────────────────────────▶  ┌──────────────┐<br>
│  Source DB   │                                           │  Target DB   │<br>
│ (Publisher)  │                                           │ (Subscriber) │<br>
└──────────────┘                                           └──────────────┘<br>
▲                                                         ▲<br>
│                                                         │<br>
│                    Replication Slot                    │<br>
└─────────────────────────────────────────────────────────┘</p>
<pre><code>             Traffic Cutover  
</code></pre>
<p>┌──────────────┐                                   ┌──────────────┐<br>
│ Application  │ ───────────── switch ───────────▶│  Target DB   │<br>
└──────────────┘                                   └──────────────┘</p>
<p>Use when:</p>
<ul>
<li>Production systems</li>
<li>Billions of rows</li>
<li>Downtime must be minimal</li>
</ul>
<h4>4. Physical Replication (pg_basebackup)</h4>
<p>┌──────────────┐      Base Backup + WAL Files      ┌──────────────┐<br>
│  Source DB   │ ───────────────────────────────▶ │  Replica DB  │<br>
│ (Primary)    │                                   │ (Cloud B)    │<br>
└──────────────┘                                   └──────────────┘<br>
│<br>
▼<br>
Promote to Primary</p>
<p>Use when:</p>
<ul>
<li>Same PostgreSQL version</li>
<li>Compatible storage</li>
<li>Very large datasets</li>
<li>Advanced DBA team</li>
</ul>
<h4>5. Enterprise-Grade Migration with Staging Layer</h4>
<p>┌─────────────────┐<br>
│  Monitoring &#x26;    │<br>
│  Validation      │<br>
└────────┬────────┘<br>
│<br>
▼<br>
┌──────────────┐   Logical Replication   ┌──────────────┐<br>
│  Source DB   │ ─────────────────────▶ │  Staging DB  │<br>
└──────────────┘                         └──────────────┘<br>
│<br>
│ Validation + Index build<br>
▼<br>
┌──────────────┐<br>
│  Target DB   │<br>
└──────────────┘<br>
▲<br>
│<br>
Application Cutover</p>
<p>Use when:</p>
<ul>
<li>Mission-critical systems</li>
<li>Regulatory requirements</li>
<li>Very large datasets</li>
<li>Strict validation needs</li>
</ul>
<h3>Key Notes for All Architectures</h3>
<ul>
<li>Always separate schema migration from data migration</li>
<li>Never build heavy indexes before loading data</li>
<li>Always validate before traffic cutover</li>
<li>Monitor WAL growth and disk usage continuously</li>
<li>Keep rollback architecture ready</li>
</ul>
<h3>Final Thoughts</h3>
<p>Cloud database migration at TB scale is not DevOps work.</p>
<p>It is:</p>
<ul>
<li>Distributed systems engineering</li>
<li>Data architecture</li>
<li>Capacity planning</li>
<li>Risk management</li>
<li>Incident prevention</li>
</ul>
<p>Treat it like a spacecraft launch.</p>
<p>Because at this scale…</p>
<p>one wrong command doesn’t cause a bug.</p>
<p>It causes an outage.</p>
<p>A data incident.</p>
<p>Or a company-level crisis.</p>
<p>By Jatin Jain Saraf on January 11, 2026.</p>]]></content:encoded>
      <pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>PostgreSQL</category>
      <category>Cloud Migration</category>
      <category>Infrastructure</category>
    </item>
    <item>
      <title>The Hidden Architecture Behind Six Degrees of Separation</title>
      <link>https://insight.jatinjainsaraf.com/the-hidden-architecture-behind-six-degrees-of-separation</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-hidden-architecture-behind-six-degrees-of-separation</guid>
      <description>How a social theory quietly explains modern engineering, system failures, and high-impact collaboration.</description>
      <content:encoded><![CDATA[<h1>The Hidden Architecture Behind Six Degrees of Separation</h1>
<p>How a social theory quietly explains modern engineering, system failures, and high-impact collaboration.</p>
<hr>
<p>We usually hear Six Degrees of Separation as a social idea, the belief that any two people on Earth are connected by at most six relationships. But when you look at the systems we work with every day, distributed platforms, microservices, blockchain networks, DevOps pipelines, you realise something interesting: Six Degrees of Separation isn’t just a human phenomenon. It’s a technical one.</p>
<p>Modern engineering systems are deeply interconnected, sometimes visibly, mostly silently. And understanding these hidden connections can change the way we build, debug, and collaborate.</p>
<h3>1. Systems don’t exist in isolation; they exist in graphs</h3>
<p>Every backend we build is basically a living graph:</p>
<ul>
<li>Services call other services</li>
<li>APIs rely on upstream data</li>
<li>CI/CD pipelines depend on dozens of tools</li>
<li>One misconfigured environment variable affects another team’s release</li>
<li>A single network hop impacts user-facing latency</li>
</ul>
<p>Your component may feel self-contained, but in reality, it’s never more than a few “degrees” away from critical paths, just like people.</p>
<h3>2. Failures spread through weak links, not major roads</h3>
<p>Most outages don’t start with big failures. They start with:</p>
<ul>
<li>One stale cache</li>
<li>One retry loop is misbehaving</li>
<li>One schema mismatch</li>
<li>One outdated node module</li>
<li>One engineer assumes “nobody else depends on this”</li>
</ul>
<p>And like social connections, these tiny issues cascade across services that are only a couple of hops away. Outages propagate the same way news travels in a social network, quietly at first, then everywhere.</p>
<h3>3. Communication is infrastructure</h3>
<p>In engineering teams, communication behaves like a network protocol. When one link breaks, missing documentation, a misaligned expectation, or unclear ownership, the impact doesn’t stay local. It radiates. You might be 2–3 messaging hops away from someone who needs clarity from you. You may never speak to them directly, but your work will reach them. This is why I often say:</p>
<blockquote>
<p><em>“Your real customers aren’t always the people you talk to</em></p>
</blockquote>
<blockquote>
<p><em>they’re the people 3–4 degrees away consuming your output silently.”</em></p>
</blockquote>
<h3>4. Good architecture reduces the degrees of separation</h3>
<p>Strong systems intentionally reduce the number of jumps between components:</p>
<ul>
<li>Clear API contracts</li>
<li>Reliable interfaces</li>
<li>Proper versioning</li>
<li>Predictable event flows</li>
<li>Observability baked in</li>
<li>Cross-team alignment on SLAs and failure scenarios</li>
</ul>
<p>When components understand each other clearly, the system resembles a well-connected social network, fast, robust, and transparent. When they don’t, it becomes a dysfunctional web full of bottlenecks.</p>
<h3>5. The human graph and the tech graph overlap</h3>
<p>This part is usually ignored. Behind every API call, there is a team. Behind every service, there is ownership. Behind every dependency there is a relationship. Technology and people fail the same way:</p>
<ul>
<li>unclear communication</li>
<li>ambiguous ownership</li>
<li>assumptions</li>
<li>outdated information</li>
<li>isolated decision-making</li>
</ul>
<p>The technical graph can only be as strong as the human graph behind it. And that is exactly what Six Degrees of Separation teaches us.</p>
<h3>The takeaway</h3>
<p>Six Degrees of Separation isn’t just a social curiosity. It’s a design principle. It reminds us that:</p>
<ul>
<li>Every decision travels further than you expect</li>
<li>Every change affects someone you will never meet</li>
<li>Every service is only a few hops away from a user experience</li>
<li>Every engineer is part of a much bigger system</li>
</ul>
<p>We build better when we recognise the invisible connections, in code, in architecture, and in collaboration. Because in today’s world, nothing is truly isolated. Not people. Not systems. Not engineering decisions.</p>
<p>By Jatin Jain Saraf on December 11, 2025.</p>]]></content:encoded>
      <pubDate>Thu, 11 Dec 2025 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Architecture</category>
      <category>System Design</category>
    </item>
    <item>
      <title>The Complete Developer’s Guide to Indexing Assets on Supra &amp; Aptos (Coins, FAs, NFTs)</title>
      <link>https://insight.jatinjainsaraf.com/the-complete-developers-guide-to-indexing-assets-on-supra-aptos-coins-fas-nfts</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-complete-developers-guide-to-indexing-assets-on-supra-aptos-coins-fas-nfts</guid>
      <description>As a developer building on a high-performance blockchain like Supra, you’ll soon encounter a critical challenge</description>
      <content:encoded><![CDATA[<h1>The Complete Developer’s Guide to Indexing Assets on Supra &#x26; Aptos (Coins, FAs, NFTs)</h1>
<p>As a developer building on a high-performance blockchain like Supra, you’ll soon encounter a critical challenge: indexing and understanding…</p>
<hr>
<h3>The Complete Developer’s Guide to Indexing Assets on Supra &#x26; Aptos (Coins, FAs, NFTs)</h3>
<p>As a developer building on a high-performance blockchain like Supra, you’ll soon encounter a critical challenge: indexing and understanding digital assets. The chain’s history is split into two distinct eras: the legacy standards (like 0x1::coin and 0x3::token) and the modern, object-based standards (like 0x1::fungible_asset and 0x4::digital_asset).</p>
<p>These standards are not interchangeable. They have fundamentally different data models, storage locations, and require completely different indexing strategies. One is simple; the other is a minefield of potential data corruption that will keep you up at night.</p>
<p>For the past 2.5 years, our team at Supra has been building the SupraScan explorer. This is the guide we wish we had. We’ll cover everything from the core data models to the exact RPC calls and, most importantly, the hard-won “corner cases” you will hit.</p>
<h3>Part 1: The Move Data Model</h3>
<p>It’s built on the Move smart contract language, which organizes all its on-chain data around three foundational principles. The purpose of this model is Safety, Scarcity, and Flexibility.</p>
<p><strong>Modules (The Logic)</strong> A smart contract that contains all the logic (functions and procedures) for a program. Modules have no inherent storage; they define the rules for interacting with Resources and Objects. Modules can be updated and iterated upon. Module functions can only access the internal fields of Resources and Objects they define, preventing outside tampering.</p>
<ul>
<li><strong>Use Case:</strong> The Bank Vault’s Blueprint. Defines the rules for depositing, withdrawing, and securing funds, but doesn’t hold the money itself.</li>
</ul>
<p><strong>Resources (The Scarcity Primitive)</strong> A special data structure that holds state/data. Resources have Move Semantics, meaning they cannot be copied, duplicated, or implicitly dropped/destroyed (like a Rust ownership model). This ensures digital assets behave like physical assets and is fundamental to preventing double-spending.</p>
<ul>
<li><strong>Use Case:</strong> The Token Balance/Deed. Guarantees that an asset’s data (like a balance of 100 APT) can only exist in one place. It is stored <em>inside</em> an Account’s address or, more commonly now, inside an Object.</li>
</ul>
<p><strong>Objects (The Container)</strong> A user-defined, composable container that has its <em>own</em> unique address on the blockchain. An Object can hold one or more Resources and even other Objects. This is the foundation of the modern asset standards.</p>
<ul>
<li><strong>Use Case:</strong> The House/Car. A single, unique entity (the Object) with a unique address that contains multiple resources (e.g., the Deed, the Vehicle ID, the Insurance Policy).</li>
</ul>
<h3>Part 2: Fungible Tokens (The “Money”) — FA vs. Coin</h3>
<p>This covers standard, interchangeable tokens like USDC or the native SRA/APT coin.</p>
<p><strong>Fungible Assets (FA): The New 0x1::fungible_asset Standard</strong></p>
<ul>
<li><strong>Philosophy:</strong> Object-Based.</li>
<li><strong>How it Works:</strong> The FA standard uses Objects for metadata and storage. The asset’s metadata (name, symbol) is an Object, and a user’s balance is stored in a FungibleStore resource on an object owned by the user.</li>
<li><strong>Use Case:</strong> Creating a stablecoin ($USDC). The FA model ensures all metadata is stored on-chain in an Object, simplifying integration for wallets and exchanges. It’s flexible and allows for advanced features like custom transfer logic.</li>
</ul>
<p><strong>Coin Legacy: The Old 0x1::coin Standard</strong></p>
<ul>
<li><strong>Philosophy:</strong> Resource-Based.</li>
<li><strong>How it Works:</strong> To hold a “Coin,” a user must have a CoinStore resource inside their account. This resource contains a simple value field, which is their balance.</li>
<li><strong>The Indexer’s Challenge:</strong> You cannot ask an account, “Show me all coins you own.” You must know the CoinType in advance to query the specific resource.</li>
<li><strong>Explorer Solution:</strong> An explorer must first build a master list of all CoinInfo resources on the chain, then query a user’s account for every single coin in your master list to build their portfolio.</li>
<li><strong>Events:</strong> Track 0x1::coin::DepositEvent and 0x1::coin::WithdrawEvent.</li>
</ul>
<h3>Part 3: Non-Fungible Tokens (The “Collectibles”)</h3>
<p>This is the most complex part of indexing, as the two standards are radically different.</p>
<h3>Part 4: The 0x3 Legacy Token Standard (The “Hard Mode”)</h3>
<p><strong>PHILOSOPHY: THE “BANK VAULT” MODEL</strong> The asset’s metadata (TokenData) is stored in the <em>creator’s</em> account. The asset’s instance (Token) is stored in the <em>owner’s</em> account. The data is completely separate and spread across the chain.</p>
<p><strong>KEY STRUCTS (THE METADATA)</strong></p>
<ul>
<li><strong>CollectionData:</strong> Contains metadata for an entire collection (name, description, uri, maximum).</li>
<li><strong>TokenData:</strong> Contains metadata for a <em>specific</em> token (name, uri, royalty, properties).</li>
<li><strong>Royalty:</strong> Defined per-token in this standard.</li>
<li><strong>Properties (PropertyMap):</strong> On-chain key-value traits</li>
<li><strong>Mutability:</strong> Booleans for which fields can be edited (e.g., uri: false). An explorer MUST show this.</li>
</ul>
<p><strong>KEY IDENTIFIERS (THE “HOW”)</strong></p>
<ul>
<li><strong>TokenDataId:</strong> The unique key for the token’s metadata. It’s a 3-part struct: creator (address), collection (String), and name (String). This is your primary key.</li>
<li><strong>TokenId:</strong> The unique key for the <em>instance</em> a user owns. It’s a struct containing the TokenDataId and a property_version (usually “0”).</li>
</ul>
<p><strong>ON-CHAIN STORAGE (THE “WHERE”)</strong></p>
<ul>
<li><strong>Collections Resource (On Creator’s Account):</strong> This resource (0x3::token::Collections) is the key for backtracking. It contains two Table HANDLES (pointers) to the CollectionData and TokenData tables.</li>
<li><strong>TokenStore Resource (On Owner’s Account):</strong> This resource (0x3::token::TokenStore) exists in <em>every</em> owner’s account. It contains a tokens handle, which points to a table of Token structs that this specific wallet owns.</li>
</ul>
<p><strong>THE PROBLEM: FINDING THE 0x3 OWNER</strong> You cannot ask the blockchain, “Who owns this token?” There is no central owner field. To find the owner, you would have to scan every TokenStore on every account, which is impossible.</p>
<p><strong>THE SOLUTION: A TWO-SYSTEM INDEXER</strong></p>
<p>You MUST build two systems to handle this.</p>
<p><strong>System 1: The “Fast” Event Tracker</strong> To listen to all events, from genesis, and build its own database of ownership.</p>
<ul>
<li><strong>Key Events:</strong> CreateCollectionEvent, CreateTokenDataEvent, DepositEvent (Ownership Gained), WithdrawEvent (Ownership Lost).</li>
<li><strong>The Flaw:</strong> If your indexer’s RPC call fails, misses a single block, and that block lands in the dead-letter queue, your database will be permanently stale.</li>
</ul>
<p><strong>System 2: The “Slow” Reconciliation Validator</strong> This is the <em>only</em> way to solve the stale data problem. It’s a background worker that constantly health-checks your database.</p>
<ol>
<li><strong>Job:</strong> It runs in a loop, querying your <em>own</em> DB for 0x3 tokens.</li>
<li><strong>Verify Owner:</strong> It takes the ownerId you have on file and runs the 2-step RPC check to ask the blockchain: “Does Owner A <em>still</em> own this token?” (Get TokenStore handle -> Query Table Item).</li>
<li><strong>The Result:</strong> If the RPC returns 404 Not Found, you have detected an error. Your data is stale. You cannot find the new owner, but you can <em>fix your database</em> by setting the ownerId to UNKNOWN.</li>
</ol>
<h3>Part 5: The 0x4 Digital Asset (DA) Standard (The “Easy Mode”)</h3>
<p><strong>PHILOSOPHY: THE “ASSET IS THE ACCOUNT” MODEL</strong> This standard is a dream for indexers. Every NFT and every Collection is a standalone Object with its own unique address.</p>
<p><strong>KEY STRUCTS (RESOURCES ON THE OBJECT)</strong></p>
<ul>
<li><strong>0x1::object::ObjectCore:</strong> The most important resource.</li>
<li><strong>owner (address):</strong> THIS IS IT. The single, authoritative field for the current owner.</li>
<li><strong>0x4::collection::Collection:</strong> Holds name and URI.</li>
<li><strong>0x4::royalty::Royalty:</strong> Defines the royalty per-collection.</li>
<li><strong>0x4::token::Token:</strong> Holds the link to the collection and the URI.</li>
<li><strong>Warning:</strong> The ‘name’ field here is often empty (“”).</li>
<li><strong>0x4::token::TokenIdentifiers:</strong> Holds the <em>real</em> display name (e.g., “Supra Spike #1”).</li>
</ul>
<p><strong>THE SOLUTION: FINDING THE 0x4 OWNER</strong> It’s one simple RPC call: <code>GET /accounts/{NFT_OBJECT_ADDRESS}/resource/0x1::object::ObjectCore</code> ...then just read the owner field.</p>
<p><strong>KEY 0x4 EVENTS</strong></p>
<ul>
<li><code>0x4::collection::Mint</code> (Token Created)</li>
<li><code>0x1::object::TransferEvent</code> (Ownership Changed - The ONLY event needed).</li>
</ul>
<h3>Part 6: The Universal Challenge — The Metadata URI</h3>
<p>Both 0x3 and 0x4 use a uri field to point to metadata. An indexer MUST fetch this uri to get the real image, traits, and other details.</p>
<p><strong>THE PROBLEM:</strong> The uri is just a string. It can be a JSON link, a direct image link, an on-chain Data URI, or even a website. Fetching a 2GB video file will crash your indexer.</p>
<p><strong>THE ROBUST FETCHING LOGIC (FOR YOUR BACKEND)</strong></p>
<p>Your indexer cannot just GET every uri. This is the only safe way to process them:</p>
<ol>
<li><strong>Check Prefix:</strong></li>
</ol>
<ul>
<li>If <code>data:</code> -> It's an on-chain SVG. No network call needed.</li>
<li>If <code>http/ipfs</code> -> Proceed to Step 2.</li>
</ul>
<ol>
<li><strong>Resolve URI:</strong> Convert <code>ipfs://</code> to a private gateway link (e.g., Pinata). Public gateways will block you.</li>
<li><strong>Make HEAD Request:</strong> This is the key optimization. Make a fast HEAD request to get the Content-Type header only.</li>
<li><strong>Check Content-Type:</strong></li>
</ol>
<ul>
<li>If <code>application/json</code> -> Make a full GET request, download, and parse the metadata.</li>
<li>If <code>image/png</code>, <code>video/mp4</code> -> STOP. Do not download. You know the URI is the media itself.</li>
<li>If HEAD fails -> The link is broken.</li>
</ul>
<p><strong>Conclusion</strong></p>
<p>Indexing assets on Supra/Aptos is a tale of two eras. The modern 0x4 and FA standards are robust and easy to query. The legacy 0x1 and 0x3 standards are complex and require a dedicated two-system indexer to ensure data accuracy. By handling these standards correctly, and by building a robust URI processor, you can create a fast, reliable explorer for the entire ecosystem.</p>
<p>By Jatin Jain Saraf on November 20, 2025.</p>]]></content:encoded>
      <pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>Web3</category>
      <category>Performance</category>
    </item>
    <item>
      <title>The PostgreSQL Elephant in the Room: A Deep Dive Into the Architecture That Powers Giants</title>
      <link>https://insight.jatinjainsaraf.com/the-postgresql-elephant-in-the-room-a-deep-dive-into-the-architecture-that-powers-giants</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-postgresql-elephant-in-the-room-a-deep-dive-into-the-architecture-that-powers-giants</guid>
      <description>PostgreSQL’s reputation for resilience, consistency, and performance isn’t accidental. It’s the result of d</description>
      <content:encoded><![CDATA[<h1>The PostgreSQL Elephant in the Room: A Deep Dive Into the Architecture That Powers Giants</h1>
<p>PostgreSQL’s reputation for resilience, consistency, and performance isn’t accidental. It’s the result of deliberate, brilliant…</p>
<hr>
<h3>The PostgreSQL Elephant in the Room: A Deep Dive Into the Architecture That Powers Giants</h3>
<p>PostgreSQL’s reputation for resilience, consistency, and performance isn’t accidental. It’s the result of deliberate, brilliant architectural decisions made over decades of open-source development. While many of us use it daily, few venture beneath the surface to understand <em>how</em> it all works.</p>
<p>This is that journey. We will dissect the core components of PostgreSQL, from the moment you connect to the database to the complex dance of scaling it to handle billions of transactions.</p>
<h3>Chapter 1: The Blueprint — Core Architecture &#x26; Memory</h3>
<p>PostgreSQL operates on a classic client-server model, but its stability comes from its multi-process architecture. Unlike databases that use a single process with many threads, PostgreSQL isolates connections for maximum stability.</p>
<ul>
<li><strong>The Postmaster (The Listener):</strong> This is the master daemon, the first process to start. Its primary job is to listen for incoming client connections. When a request arrives, it authenticates the user and then <strong>forks</strong> — creates an exact copy of itself — which becomes a dedicated backend process for that client.</li>
<li><strong>The Backend Process (The Workhorse):</strong> This dedicated <code>postgres</code> process handles all queries for a single client connection. This isolation is a cornerstone of PostgreSQL's legendary stability. If a complex query causes this single backend process to crash, it has <strong>zero effect</strong> on other connections or the master process. The Postmaster simply cleans it up and continues listening for new connections.</li>
</ul>
<p>These processes interact with two main areas of memory:</p>
<ul>
<li><strong>Shared Memory:</strong> This large memory block is allocated when the server starts and is accessible by all PostgreSQL processes. Its key components include:</li>
<li><strong>Shared Buffers (</strong><code>**shared_buffers**</code><strong>):</strong> This is the database's primary disk cache and the most critical performance-tuning parameter. When you request data, PostgreSQL fetches the 8KB data pages from disk and stores them here. Subsequent requests for the same data can be served directly from this fast memory cache, avoiding slow disk I/O.</li>
<li><strong>WAL Buffers (</strong><code>**wal_buffers**</code><strong>):</strong> Before changes are written to the permanent Write-Ahead Log on disk, they are staged in this small buffer. This allows the system to collect WAL data for multiple transactions and write it out in a single, efficient operation.</li>
<li><strong>Commit Log (</strong><code>**CLOG**</code><strong>):</strong> This small but vital area tracks the status of every transaction: <code>in-progress</code>, <code>committed</code>, or <code>aborted</code>. When a transaction needs to check the visibility of a row, it consults the CLOG to see if the transaction that created it was successfully committed.</li>
<li><strong>Local Memory:</strong> Each backend process has its own private memory for operations specific to its queries. Key parameters include:</li>
<li><strong>Work Memory (</strong><code>**work_mem**</code><strong>):</strong> Used for operations that need space outside of shared buffers, such as sorting data for <code>ORDER BY</code> clauses or building hash tables for joins.</li>
<li><strong>Maintenance Work Memory (</strong><code>**maintenance_work_mem**</code><strong>):</strong> A larger chunk of memory reserved for maintenance tasks like creating indexes (<code>CREATE INDEX</code>) or cleaning up tables (<code>VACUUM</code>).</li>
</ul>
<h3>Chapter 2: The Time Machine — Unraveling MVCC</h3>
<p>The magic behind PostgreSQL’s incredible concurrency is <strong>Multi-Version Concurrency Control (MVCC)</strong>. It’s how PostgreSQL allows a long-running analytics query to run at the same time as hundreds of users are writing new data, all without locking each other.</p>
<p>To understand it, you must first understand that PostgreSQL doesn’t really have “rows.” It has <strong>tuples</strong>, which are versions of a row.</p>
<ul>
<li><strong>The Life of a Tuple:</strong></li>
</ul>
<ol>
<li><code>**INSERT**</code><strong>:</strong> A new tuple is created. A hidden system column, <code>xmin</code>, is stamped with the ID of the inserting transaction.</li>
<li><code>**DELETE**</code><strong>:</strong> The tuple is not physically deleted. Instead, its hidden <code>xmax</code> column is stamped with the ID of the deleting transaction. The tuple is now a "dead row."</li>
<li><code>**UPDATE**</code><strong>:</strong> An <code>UPDATE</code> is an atomic combination of a <code>DELETE</code> and an <code>INSERT</code>. The current tuple is marked as dead by setting its <code>xmax</code>, and a completely new tuple with the updated data is created, with its <code>xmin</code> set to the current transaction ID.</li>
</ol>
<ul>
<li><strong>Transaction Snapshots and Visibility:</strong> When a query begins, the transaction takes a “snapshot” of the database. This snapshot knows which transaction IDs were committed at that exact moment. When scanning a table, the query follows these rules for each tuple it encounters:</li>
<li>Is the tuple’s <code>xmin</code> from a transaction that was committed <em>before</em> my snapshot?</li>
<li>Is the tuple’s <code>xmax</code> either null or from a transaction that was <em>not yet committed</em> when my snapshot was taken?</li>
</ul>
<p>If the answer to both is yes, the tuple is visible to the query. This system elegantly allows different transactions to see different, consistent versions of the database state at the same time. The obvious consequence of this design is the accumulation of dead tuples, leading to a phenomenon called <strong>bloat</strong>.</p>
<h3>Chapter 3: The Janitor — VACUUM and Transaction ID Wraparound</h3>
<p>Bloat from dead tuples makes tables and indexes larger than necessary, slowing down queries. The process responsible for cleaning this up is <strong>VACUUM</strong>.</p>
<p><code>VACUUM</code> performs three critical tasks:</p>
<ol>
<li><strong>Reclaiming Space:</strong> It scans tables, identifies dead tuples, and adds the space they occupy to the table’s <strong>Free Space Map (FSM)</strong>. This space is now available for future <code>INSERT</code>s and <code>UPDATE</code>s. A standard <code>VACUUM</code> does <em>not</em> return this space to the operating system; for that, you need a <code>VACUUM FULL</code>, which exclusively locks the table and rewrites it, a costly operation.</li>
<li><strong>Updating Statistics:</strong> <code>VACUUM</code> is often run with <code>ANALYZE</code>. This command scans the table's data distribution and updates internal statistics that the query planner uses to make intelligent decisions.</li>
<li><strong>Preventing Transaction ID Wraparound:</strong> This is arguably <code>VACUUM</code>'s most critical job. PostgreSQL uses a 32-bit transaction ID (XID). If this number were to grow indefinitely, it would eventually wrap around. This would be catastrophic, as transactions from the distant past would suddenly appear to be in the future, making all data invisible. <code>VACUUM</code> prevents this by "freezing" the XIDs of very old rows, marking them as permanently ancient and visible to all transactions.</li>
</ol>
<p>Because running this manually is impractical, the <strong>autovacuum</strong> daemon handles this automatically in the background, making it a cornerstone of a healthy database.</p>
<h3>Chapter 4: The Scribe — Write-Ahead Logging (WAL) and Durability</h3>
<p>The <strong>“D” in ACID (Durability)</strong> means that once a transaction is committed, it is permanent, even if the server crashes immediately after. PostgreSQL guarantees this with its <strong>Write-Ahead Log (WAL)</strong>.</p>
<p>The principle is simple: <strong>before any change is written to the actual data files on disk, a record of that change is first written and flushed to the WAL file.</strong></p>
<ul>
<li><strong>Performance:</strong> Writing sequentially to a log file is much faster than the random I/O required to update different data pages scattered across a disk. This allows PostgreSQL to batch the slower, random writes to the main data files.</li>
<li><strong>Durability:</strong> In case of a crash, PostgreSQL initiates a recovery process on startup. It reads the WAL from the last <strong>checkpoint</strong> (a known good point on disk) and methodically replays all the logged changes, bringing the database back to a perfectly consistent state.</li>
</ul>
<h3>Chapter 5: The Conductor — From SQL to Results</h3>
<p>When you send a query, it goes on a four-stage journey before results are returned.</p>
<ol>
<li><strong>Parser:</strong> Checks your SQL for correct syntax and converts the raw text into a structured <strong>parse tree</strong>.</li>
<li><strong>Analyzer/Rewriter:</strong> Checks that the tables and columns in the parse tree exist. It also expands views into their underlying queries, producing a <strong>query tree</strong>.</li>
<li><strong>Planner/Optimizer:</strong> This is the brain of the operation. It receives the query tree and generates many possible <strong>execution plans</strong>. Using the statistics gathered by <code>ANALYZE</code>, it estimates the "cost" of each plan (e.g., is it cheaper to use an index or just scan the whole table?). It then selects the plan with the lowest estimated cost. You can inspect this plan using the <code>EXPLAIN</code> command.</li>
<li><strong>Executor:</strong> This component takes the optimal plan and runs it, fetching the tuples, performing sorts and joins, and finally returning the results to you.</li>
</ol>
<h3>Chapter 6: The Architect — Scaling PostgreSQL</h3>
<p>Eventually, a single server isn’t enough. PostgreSQL offers a robust path to scaling.</p>
<ul>
<li><strong>Vertical Scaling:</strong> The simplest approach — giving the server more powerful CPU, more RAM, and faster disks. This has physical limits and becomes prohibitively expensive.</li>
<li><strong>Horizontal Read Scaling (Replication):</strong> This is achieved via <strong>Streaming Replication</strong>. A primary server streams its WAL records over the network to one or more standby replicas. A special <strong>WAL Sender</strong> process on the primary handles this. The replicas consume this WAL stream and apply the changes to their own data files, keeping them nearly in sync. This allows you to offload read-heavy queries to the replicas, freeing up the primary for writes.</li>
<li><strong>Horizontal Data Scaling (Partitioning):</strong> A native feature where a large table is logically divided into smaller physical pieces. For a blockchain indexer’s <code>transactions</code> table, you could <code>PARTITION BY RANGE</code> on the transaction date. When you query for recent transactions, the planner uses <strong>partition pruning</strong> to ignore all old partitions, leading to massive performance gains.</li>
<li><strong>Horizontal Write Scaling (Sharding):</strong> For workloads that overwhelm even a single primary writer, sharding is the answer. While not automatic, extensions like <strong>Citus Data</strong> transform PostgreSQL into a distributed cluster. Data is distributed across multiple nodes, and writes can be directed to the node that holds the relevant data shard, breaking the single-writer bottleneck.</li>
</ul>
<h3>Conclusion</h3>
<p>PostgreSQL’s architecture is a masterclass in database design. The interplay between its multi-process model for stability, MVCC for non-blocking concurrency, WAL for durability, and a sophisticated planner for performance creates a system that is far more than the sum of its parts. By understanding these internal mechanics, you move from simply using a database to truly engineering a robust, scalable, and reliable data platform.</p>
<p>By Jatin Jain Saraf on July 27, 2025.</p>]]></content:encoded>
      <pubDate>Sun, 27 Jul 2025 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>SQL</category>
      <category>Architecture</category>
      <category>System Design</category>
    </item>
    <item>
      <title>From Lag to Leading: A Deep Dive into Fixing a 2-Hour PostgreSQL Replication Lag</title>
      <link>https://insight.jatinjainsaraf.com/from-lag-to-leading-a-deep-dive-into-fixing-a-2-hour-postgresql-replication-lag</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/from-lag-to-leading-a-deep-dive-into-fixing-a-2-hour-postgresql-replication-lag</guid>
      <description>Every engineer responsible for a large database fears the “big one” — a problem so severe it brings operations to a</description>
      <content:encoded><![CDATA[<h1>From Lag to Leading: A Deep Dive into Fixing a 2-Hour PostgreSQL Replication Lag</h1>
<p>Every engineer responsible for a large database fears the “big one” — a problem so severe it brings operations to a crawl. We recently…</p>
<hr>
<h3>From Lag to Leading: A Deep Dive into Fixing a 6-Hour PostgreSQL Replication Lag</h3>
<p>Every engineer responsible for a large database fears the “big one” — a problem so severe it brings operations to a crawl. We recently faced this crisis: a high-throughput indexer processing millions of daily transactions on a powerful PostgreSQL server had its read replica fall behind by over six hours. Queries were timing out, and our system was on the brink.</p>
<p>This is the story of how we diagnosed the cascading failures and brought the system back from the edge, not just by treating the lag itself, but by fixing the underlying issues that caused it.</p>
<h3><strong>Part 1: The First Clue — Checkpoint Hell</strong></h3>
<p>Our investigation started on the primary server. It was struggling to keep up, with disk I/O and CPU usage constantly high. The replication lag was a symptom, but the disease was on the primary.</p>
<ul>
<li><strong>The Diagnostic Step:</strong> We enabled a crucial logging parameter: <code>log_checkpoints = on</code>. This gave us visibility into one of PostgreSQL's most I/O-intensive operations.</li>
<li><strong>The Smoking Gun:</strong> The logs were terrifying. We saw messages like <code>**checkpoints are occurring too frequently (11 seconds apart)**</code> and a constant stream of <code>**checkpoint starting: wal**</code>. The database was in "checkpoint hell," desperately trying to flush data to disk because it was running out of transaction log space.</li>
<li><strong>The Fix:</strong> The root cause was a <code>**max_wal_size**</code> setting that was too small for our write volume. On a powerful server with 128 GB of RAM, the default 1 GB was a drop in the ocean. We increased it to <strong>16 GB</strong>.</li>
<li><strong>The Result:</strong> The effect was immediate. The frantic, size-based checkpoints stopped. The logs shifted to a calm, predictable <code>**checkpoint starting: time**</code>, occurring every 5 minutes. The primary server was stable, but our work wasn't done.</li>
</ul>
<h3>Part 2: The Second Problem — A Bloated Replica</h3>
<p>Despite a healthy primary, the replica was still lagged and slow. The bottleneck had shifted. It wasn’t about the primary <em>sending</em> data anymore; it was about the replica’s ability to <em>receive and apply</em> it.</p>
<ul>
<li><strong>The Diagnostic Step:</strong> We queried the <code>pg_stat_all_tables</code> view to inspect the health of our tables.</li>
<li><strong>The Horrifying Discovery:</strong> We found severe <strong>table bloat</strong>. One critical table had over <strong>44 million dead tuples</strong>. The replica was a digital graveyard. Every query and every replicated write had to sift through this mountain of junk data, killing performance.</li>
<li><strong>The Quick Check:</strong> We also confirmed that <code>**hot_standby_feedback**</code> was enabled on the replica. This is a critical setting that prevents long-running read queries from causing replication conflicts.</li>
</ul>
<h3>Part 3: The Cleanup — Unleashing Autovacuum</h3>
<p>The path forward was clear: we had to get rid of the bloat. Instead of a one-time manual <code>VACUUM</code>, we opted to create a sustainable, automated solution by making the <code>autovacuum</code> process hyper-aggressive.</p>
<ul>
<li><strong>Global Tuning:</strong> First, we increased the number of workers and made them faster by setting <code>**autovacuum_max_workers = 6**</code> and <code>**autovacuum_vacuum_cost_delay = 2ms**</code>.</li>
<li><strong>The Critical Per-Table Fix:</strong> The most important change was telling <code>autovacuum</code> <em>when</em> to start. For our massive tables, the default trigger (20% of rows changed) would never happen. We ran the following command on our most bloated tables:</li>
</ul>
<p>ALTER TABLE your_bloated_table SET (autovacuum_vacuum_scale_factor = 0.001);</p>
<ul>
<li>This forced <code>autovacuum</code> to run after only 0.1% of rows changed, ensuring constant, proactive cleanup.</li>
</ul>
<h3>Part 4: Interpreting the Healing Phase 🩺</h3>
<p>After applying the <code>autovacuum</code> settings, we saw a curious pattern: the <code>checkpoint starting: wal</code> messages returned. For a moment, it felt like a step backward, but it was actually a sign of success.</p>
<p>The newly aggressive <code>autovacuum</code> was working so hard cleaning up the bloat that its own operations were generating a massive amount of WAL. This activity was so intense that it temporarily outpaced our 5-minute checkpoint timer. The database wasn't in trouble; it was healing itself. After a few hours, as the worst of the bloat was cleared, the WAL generation subsided, and the checkpoints returned to their stable, time-based schedule.</p>
<h3>Conclusion: Our Key Takeaways</h3>
<p>This experience reinforced several core database administration principles:</p>
<ol>
<li><strong>Replication lag is a symptom, not the disease.</strong> The root causes were I/O bottlenecks and table bloat.</li>
<li><strong>Tune your primary first.</strong> A stable primary is the foundation of healthy replication. Ensure <code>max_wal_size</code> is appropriate for your workload and hardware.</li>
<li><strong>Bloat is a silent killer.</strong> For write-intensive systems, aggressive <code>autovacuum</code> isn't just a good idea; it's a requirement. Don't trust the default settings on very large tables.</li>
<li><strong>Monitor everything.</strong> You can’t fix what you can’t see. Using tools like <code>log_checkpoints</code> and system views like <code>pg_stat_all_tables</code> is essential for proper diagnosis.</li>
<li><strong>Be patient.</strong> After applying a fix, give the system time to react and stabilize. The “healing phase” can look noisy, but it’s often a sign that your solution is working.</li>
</ol>
<p>#PostgreSQL #DatabaseTuning #ReplicationLag #PerformanceTuning #DevOps #SRE #Database #HighAvailability #SoftwareEngineering #Tech #CaseStudy #Backend #CloudSQL</p>
<p>By Jatin Jain Saraf on July 23, 2025.</p>]]></content:encoded>
      <pubDate>Wed, 23 Jul 2025 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>SQL</category>
      <category>PostgreSQL</category>
      <category>Performance</category>
    </item>
    <item>
      <title>Taming PostgreSQL Replication Lag in Real-Time Blockchain Indexers</title>
      <link>https://insight.jatinjainsaraf.com/taming-postgresql-replication-lag-in-real-time-blockchain-indexers</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/taming-postgresql-replication-lag-in-real-time-blockchain-indexers</guid>
      <description>In distributed systems that process massive volumes of real-time data, such as blockchain indexers, database replication is essent</description>
      <content:encoded><![CDATA[<h1>Taming PostgreSQL Replication Lag in Real-Time Blockchain Indexers</h1>
<p>In distributed systems that process massive volumes of real-time data, such as blockchain indexers, database replication is essential for…</p>
<hr>
<h3>Taming PostgreSQL Replication Lag in Real-Time Blockchain Indexers</h3>
<p>In distributed systems that process massive volumes of real-time data, such as blockchain indexers, <strong>database replication</strong> is essential for scalability, fault tolerance, and read optimization. However, when replication isn’t tuned for high-ingestion workloads, it can lead to delays known as <strong>replication lag</strong>.</p>
<p>This challenge is especially relevant in systems built on <strong>PostgreSQL</strong>, where replication is robust but also sensitive to write intensity and resource limits.</p>
<h3>What is Replication Lag?</h3>
<p><strong>Replication lag</strong> is the delay between a write occurring on the <strong>primary database</strong> and the time that the change appears on one or more <strong>replica databases</strong>. In systems that route read-heavy traffic to replicas (a.k.a. read replicas or followers), replication lag can result in <strong>stale reads,</strong> inconsistencies between the data being written and what users or services are reading.</p>
<h3>Why Indexers Suffer from Replication Lag</h3>
<p>Indexers like those powering blockchain explorers, real-time search engines, or analytics dashboards are constantly ingesting and storing high volumes of data. Common patterns include:</p>
<ul>
<li>Thousands to millions of insertions per minute</li>
<li>Frequent bulk writes and upserts</li>
<li>Continuous schema changes or reprocessing cycles</li>
</ul>
<p>These stress the <strong>primary database</strong>, but the <strong>replication overhead</strong> falls on the replicas, especially in <strong>PostgreSQL</strong>, which uses <strong>Write-Ahead Logging (WAL)</strong> to maintain consistency.</p>
<h3>Real-World Impact of Replication Lag</h3>
<ul>
<li><strong>Stale Reads:</strong> Applications querying replicas may not see the most recent data.</li>
<li><strong>Consistency Issues:</strong> Time-sensitive operations (e.g., monitoring alerts, transaction lookups) may behave unpredictably.</li>
<li><strong>Debugging Complexity:</strong> Developers and users may encounter discrepancies between data seen on the UI and what’s actually stored.</li>
</ul>
<h3>How PostgreSQL Replication Works (Under the Hood)</h3>
<p>PostgreSQL uses <strong>physical streaming replication</strong> as its most common replication method. Here’s how it works:</p>
<ol>
<li><strong>WAL Generation:</strong><br>
 All changes to the database are first written to <strong>WAL (Write-Ahead Log) segments</strong>.</li>
<li><strong>WAL Shipping to Replicas:</strong><br>
 The primary sends these WAL files to replicas over a TCP connection.</li>
<li><strong>WAL Replay:</strong><br>
 Replicas <strong>replay the WAL records</strong> to reflect the exact state of the primary.</li>
<li><strong>Feedback Loop:</strong><br>
 Replicas send <strong>acknowledgements</strong> (in async setups) indicating the LSN (Log Sequence Number) up to which they’ve replayed.</li>
</ol>
<h3>Why Replication Lag Happens in PostgreSQL</h3>
<ol>
<li><strong>WAL Replay Bottlenecks:</strong><br>
 If the replica can’t <strong>replay WAL fast enough</strong> (due to disk I/O or CPU limitations), it lags.</li>
<li><strong>Network Latency:</strong><br>
 Even if WAL files are generated quickly, slow transfer over the network delays replication.</li>
<li><strong>Disk IOPS Limitation:</strong><br>
 Replay of WAL on the replica involves <strong>I/O-intensive operations</strong>. If the replica’s disk isn’t fast enough, WAL gets backlogged.</li>
<li><strong>Hot Standby Conflicts:</strong><br>
 In replicas configured for read queries (<code>hot_standby = on</code>Long-running queries can conflict with WAL replay—PostgreSQL may delay applying WAL to avoid cancelling queries.</li>
<li><strong>Large Transactions:</strong><br>
 WAL size increases dramatically for bulk operations. Replicas have to <strong>process the entire transaction before replaying</strong>, delaying visibility.</li>
<li><strong>Checkpointing and Vacuuming:</strong><br>
 Postgres background processes like <code>checkpointer</code> and <code>autovacuum</code> can stall WAL replay if the system is already under pressure.</li>
</ol>
<h3>Why Not Use Synchronous Replication?</h3>
<p>While <strong>synchronous replication</strong> guarantees <strong>strong consistency</strong>, it’s not suitable for indexers due to:</p>
<ul>
<li><strong>Performance Impact:</strong> The primary must <strong>wait for at least one replica to confirm each write</strong>, increasing write latency significantly.</li>
<li><strong>Reduced Throughput:</strong> High-volume insert operations slow down to match the speed of the slowest participating replica.</li>
<li><strong>Availability Risks:</strong> If a replica goes offline or slows down, the primary may <strong>block new writes,</strong> a dangerous bottleneck in real-time systems.</li>
</ul>
<h3>Why We Use Eventual Consistency Instead</h3>
<p>For blockchain indexers, <strong>eventual consistency</strong> via <strong>asynchronous replication</strong> is more practical:</p>
<ul>
<li>High throughput for inserts</li>
<li>Independent, scalable reads from replicas</li>
<li>Non-blocking writes, even if replicas lag briefly</li>
</ul>
<p>For <strong>critical paths</strong> like user-triggered transaction lookups, we directly query the <strong>primary</strong> to ensure data freshness. How to Mitigate Replication Lag</p>
<h3><strong>Strategies to Reduce Replication Lag</strong></h3>
<ol>
<li><strong>Optimize Writes</strong><br>
 Break down bulk inserts into smaller chunks or use batched writes strategically. Avoid unnecessary updates or deletes.</li>
<li><strong>Use Synchronous Replication for Critical Data</strong><br>
 For highly sensitive operations, synchronous replication ensures consistency at the cost of some performance.</li>
<li><strong>Scaling Replicas Vertically or Horizontally</strong><br>
 Add more compute resources (CPU, memory, IOPS) to replicas or distribute reads across more nodes.</li>
<li><strong>Monitoring &#x26; Alerting</strong><br>
 Set up real-time monitoring (e.g., PostgreSQL’s <code>pg_stat_replication</code> or MySQL’s <code>Seconds_Behind_Master</code>) to catch replication lag early.</li>
<li><strong>Dedicated Replication Channels</strong><br>
 If supported, isolate replication traffic from normal network traffic to prevent interference.</li>
<li><strong>Prioritise Write Load Management</strong><br>
 If possible, buffer high-frequency writes using queues or streams (Kafka, RabbitMQ) and process them with controlled throughput.</li>
</ol>
<h3>Tips to Investigate &#x26; Reduce Replication Lag in PostgreSQL</h3>
<p><strong>Monitor</strong> <code>**pg_stat_replication**</code><br>
 Use this built-in view to see:</p>
<ul>
<li><code>write_lag</code>WAL write delay</li>
<li><code>flush_lag</code>WAL flush delay</li>
<li><code>replay_lag</code>WAL replay delay</li>
</ul>
<p>Example query:<br>
SELECT pid, client_addr, state, write_lag, flush_lag, replay_lag<br>
FROM pg_stat_replication;</p>
<ul>
<li><strong>Check WAL Size Growth</strong><br>
 Monitor WAL generation rate via tools like <code>pg_stat_wal</code> or <code>pg_stat_bgwriter</code>.</li>
<li><strong>Tune</strong> <code>**max_wal_size**</code><strong>,</strong> <code>**checkpoint_timeout**</code><strong>, and</strong> <code>**wal_compression**</code><br>
 These affect how WAL is generated and retained.</li>
<li><strong>Use</strong> <code>**archive_mode = on**</code> <strong>+ WAL Archiving</strong><br>
 Useful for delayed replicas or if streaming replication is interrupted.</li>
<li><strong>Tune Disk I/O &#x26; IOPS</strong><br>
 Make sure replicas are on high-throughput storage (e.g., NVMe, SSDs).</li>
<li><strong>Enable Logging of Replication Delays</strong><br>
 Helps identify problematic patterns: log_replication_commands = on</li>
<li><strong>Avoid Long-Running Queries on Replicas</strong><br>
 They can block WAL replay. Monitor using:</li>
</ul>
<p>SELECT pid, age(now(), query_start), query FROM pg_stat_activity<br>
WHERE state = 'active' AND backend_type = 'client backend';</p>
<h3>Observability = Diagnosability</h3>
<p>Replication lag should never be a guessing game. PostgreSQL gives you precise views into <strong>where</strong> the bottleneck is:</p>
<ul>
<li><strong>WAL Write Lag</strong> (on primary)</li>
<li><strong>WAL Shipping Lag</strong> (network)</li>
<li><strong>WAL Replay Lag</strong> (on replica)</li>
</ul>
<p>With this observability, you can implement targeted and effective solutions.</p>
<h3>Conclusion</h3>
<p>In high-ingestion, real-time systems like blockchain indexers, <strong>replication lag is an inevitable but manageable challenge</strong>, especially with PostgreSQL’s WAL-based architecture.</p>
<p>By embracing <strong>eventual consistency</strong>, designing for latency-tolerant reads, and monitoring replication health with PostgreSQL’s native tools, you can build systems that are:</p>
<ul>
<li>Fast</li>
<li>Reliable</li>
<li>Scalable</li>
</ul>
<p>With the right strategy and observability, replication lag becomes just another solvable layer in your system architecture, not a blocker.</p>
<p><strong>TL;DR</strong></p>
<ul>
<li>Replication lag in PostgreSQL affects read consistency in high-ingestion systems like blockchain indexers.</li>
<li>Causes include WAL replay lag, network issues, and hot standby conflicts.</li>
<li>Use async replication for throughput and query primaries for critical paths.</li>
<li>Monitor with<code>pg_stat_replication</code>, tune WAL settings, and optimise write patterns.</li>
</ul>
<p>By Jatin Jain Saraf on July 10, 2025.</p>]]></content:encoded>
      <pubDate>Thu, 10 Jul 2025 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Database</category>
      <category>SQL</category>
      <category>Web3</category>
      <category>Blockchain</category>
      <category>PostgreSQL</category>
      <category>Performance</category>
    </item>
    <item>
      <title>Understanding Source Code vs Bytecode in Move-Based Blockchains</title>
      <link>https://insight.jatinjainsaraf.com/understanding-source-code-vs-bytecode-in-move-based-blockchains</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/understanding-source-code-vs-bytecode-in-move-based-blockchains</guid>
      <description>In Move-based blockchains such as Aptos or Sui, smart contracts are written and deployed using the Move language. While developers in</description>
      <content:encoded><![CDATA[<h1>Understanding Source Code vs Bytecode in Move-Based Blockchains</h1>
<p>In Move-based blockchains such as Aptos or Sui, smart contracts are written and deployed using the Move language. While developers interact…</p>
<hr>
<h3>Understanding Source Code vs Bytecode in Move-Based Blockchains</h3>
<p>In Move-based blockchains such as Aptos or Sui, smart contracts are written and deployed using the Move language. While developers interact with source code, what runs on-chain is the compiled bytecode. Understanding the distinction between these two is essential for transparency, security, and trust in decentralised applications.</p>
<p><strong>What is Source Code?</strong></p>
<p>Source code refers to the original, human-readable logic written by developers using the Move programming language. It typically includes modules, functions, type definitions, and comments, and it is structured to be easy to read and understand.</p>
<p><strong>For example:</strong></p>
<p><code>module MyToken {    public fun mint(recipient: address, amount: u64) {    // mint logic    }    }</code></p>
<p>This is the blueprint developers write, review, and maintain to define how the smart contract behaves.</p>
<p><strong>What is Bytecode?</strong></p>
<p>Bytecode is the compiled, binary representation of the Move source code. It is the program version deployed to and executed by the Move Virtual Machine (MoveVM) on the blockchain.</p>
<p>Bytecode is generated by the Move compiler, such as<code>movec</code>, and appears in hexadecimal or binary format. It is not human-readable and is optimised for execution by the blockchain runtime. Once deployed, the bytecode is stored on-chain and cannot be changed.</p>
<p><strong>Why Are Source Code and Bytecode Different?</strong></p>
<p>There are several reasons why they may differ:</p>
<ol>
<li>The compilation process transforms the source code by stripping out comments, formatting, and converting high-level logic into low-level operations.</li>
<li>Compilers often apply optimisations for performance and compatibility with the virtual machine.</li>
<li>In some cases, developers may upload source code that doesn’t exactly match the deployed bytecode — either unintentionally or deliberately.</li>
</ol>
<p><strong>Implications for Trust and Transparency</strong></p>
<p>These differences can create serious implications:</p>
<ul>
<li>If users trust the displayed source code without verifying the bytecode, they may be misled.</li>
<li>Auditors could spend time analyzing source code that isn’t actually executed.</li>
<li>Malicious developers could exploit this mismatch to hide unwanted logic.</li>
</ul>
<p><strong>Why Do Blockchain Explorers Display a Warning?</strong></p>
<p>Many explorers include a warning like:</p>
<p>“The source code is plain text uploaded by the deployer, which can be different from the actual bytecode.”</p>
<p>This line is meant to caution users that what they see on the explorer may not represent the actual logic being executed on-chain. It encourages users to verify the contract to ensure transparency and trust.</p>
<p><strong>Source Code Verification</strong></p>
<p>To solve this issue, some explorers offer a “Verify Source Code” feature. This process involves:</p>
<ol>
<li>Uploading the source code and build settings.</li>
<li>Recompiling it using the same compiler version and flags.</li>
<li>Comparing the output bytecode with what’s stored on-chain.</li>
<li>Marking the contract as verified if there’s a perfect match.</li>
</ol>
<p>This builds confidence that the code users are reading is exactly what the blockchain is running.</p>
<p><strong>Conclusion</strong></p>
<p>In summary, source code is the readable logic written by developers, while bytecode is the machine-readable version that actually runs on the blockchain. Because only bytecode is deployed and executed, and since source code can be different or even misleading, explorers issue a warning and offer verification features. Verifying contracts helps the ecosystem remain secure, auditable, and trustworthy.</p>
<p>By Jatin Jain Saraf on May 7, 2025.</p>]]></content:encoded>
      <pubDate>Wed, 07 May 2025 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Web3</category>
      <category>Blockchain</category>
    </item>
    <item>
      <title>Demystifying API Gateway Patterns: Design Choices, Real-World Use Cases &amp; When to Use What</title>
      <link>https://insight.jatinjainsaraf.com/demystifying-api-gateway-patterns-design-choices-real-world-use-cases-when-to-use-what</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/demystifying-api-gateway-patterns-design-choices-real-world-use-cases-when-to-use-what</guid>
      <description>🔍 Demystifying API Gateway Patterns: Design Choices, Real-World Use Cases &amp; When to Use What In today’s rapidly evolving software world, especially where microservices and distributed systems domi</description>
      <content:encoded><![CDATA[<h1>🔍 Demystifying API Gateway Patterns: Design Choices, Real-World Use Cases &#x26; When to Use What</h1>
<p>In today’s rapidly evolving software world, especially where microservices and distributed systems dominate, API gateways have become more…</p>
<hr>
<h3>🔍 Demystifying API Gateway Patterns: Design Choices, Real-World Use Cases &#x26; When to Use What</h3>
<p>In today’s rapidly evolving software world, especially where microservices and distributed systems dominate, <strong>API gateways</strong> have become more than reverse proxies — they’re the cornerstone of <strong>secure, scalable, and maintainable</strong> architectures.</p>
<p>But with multiple gateway patterns, how do you choose the one that fits your architecture best?</p>
<p>In this blog, we’ll explore:</p>
<ul>
<li>What an API gateway is (beyond the buzzwords)</li>
<li>The role it plays in modern systems</li>
<li>5 architectural patterns for deploying API gateways</li>
<li>Real-world use cases for each</li>
</ul>
<h3>🚪 What Is an API Gateway — And Why Should You Care?</h3>
<p>Think of an API gateway as the <strong>front desk</strong> of your application’s backend. It’s the one point of contact that every external client (mobile apps, web apps, third-party integrations) interacts with before reaching your internal services.</p>
<p>But it’s more than a router:</p>
<ul>
<li>It enforces <strong>security policies</strong></li>
<li>Aggregates or transforms responses</li>
<li>Monitors and logs incoming/outgoing traffic</li>
<li>Handles rate limiting, retries, and timeouts</li>
<li>Translates protocols (e.g., REST to gRPC)</li>
</ul>
<p>In short, an API gateway <strong>simplifies</strong> client interactions while <strong>centralizing control</strong> of your backend traffic.</p>
<h3>🔄 The Rise of Modern API Gateways</h3>
<p>Older gateway solutions (like NGINX or Apache-based proxies) still serve basic needs, but today’s cloud-native environments demand more.</p>
<p>That’s where modern solutions like <strong>Solo.io’s Gloo Gateway</strong> or <strong>Kong</strong>, built on powerful proxies like <strong>Envoy</strong>, come into play.</p>
<p>Why they matter:</p>
<ul>
<li>Designed for Kubernetes and microservices</li>
<li>Support for advanced routing, service discovery</li>
<li>First-class support for <strong>WebAssembly</strong>, <strong>GraphQL</strong>, <strong>gRPC</strong></li>
<li>Rich ecosystem of plugins and integrations</li>
</ul>
<p>Whether you’re running on bare metal, Kubernetes, or a hybrid cloud — modern API gateways are built to scale, perform, and adapt.</p>
<h3>🧩 5 Common API Gateway Deployment Patterns</h3>
<p>Let’s dive into the different ways you can structure your gateway layer. Each has its pros, cons, and ideal use cases.</p>
<h4><strong>1. Centralized Gateway Pattern</strong></h4>
<p><strong>Overview:</strong> A single entry point for all external API calls. The gateway sits at the edge and handles everything — authentication, routing, rate-limiting, etc.</p>
<p><strong>🧠 Ideal for:</strong></p>
<ul>
<li>Simpler systems</li>
<li>Monolithic-to-microservices transitions</li>
<li>MVPs or startups</li>
</ul>
<p><strong>✅ Pros:</strong></p>
<ul>
<li>Easier to manage and monitor</li>
<li>Centralized policy enforcement</li>
<li>Low operational complexity</li>
</ul>
<p><strong>⚠️ Cons:</strong></p>
<ul>
<li>Can become a bottleneck</li>
<li>Single point of failure (unless highly available)</li>
</ul>
<h3>2. Tiered (Multi-Level) Gateway Pattern</h3>
<p><strong>Overview:</strong> Requests first hit an <strong>external gateway</strong> (client-facing) and then flow through an <strong>internal gateway</strong> that talks to microservices.</p>
<p><strong>🧠 Ideal for:</strong></p>
<ul>
<li>Enterprises with strict separation between external/internal traffic</li>
<li>Regulated industries (finance, healthcare)</li>
</ul>
<p><strong>✅ Pros:</strong></p>
<ul>
<li>Extra security layer</li>
<li>Internal traffic is abstracted from public clients</li>
<li>Enables fine-grained routing at the internal level</li>
</ul>
<p><strong>⚠️ Cons:</strong></p>
<ul>
<li>Slightly higher latency</li>
<li>More moving parts = more complexity</li>
</ul>
<h3>3. Service-Level (Microgateway) Pattern</h3>
<p><strong>Overview:</strong> Each service or group of services has its own mini gateway.</p>
<p><strong>🧠 Ideal for:</strong></p>
<ul>
<li>Polyglot microservice environments</li>
<li>Autonomous development teams</li>
</ul>
<p><strong>✅ Pros:</strong></p>
<ul>
<li>Teams own their own API contracts and security rules</li>
<li>Flexible versioning and rollout strategies</li>
</ul>
<p><strong>⚠️ Cons:</strong></p>
<ul>
<li>Operationally intensive</li>
<li>Harder to enforce organization-wide standards</li>
</ul>
<h3>4. Pod-Level Gateway Pattern (Kubernetes-centric)</h3>
<p><strong>Overview:</strong> A dedicated gateway instance (or sidecar proxy) per pod.</p>
<p><strong>🧠 Ideal for:</strong></p>
<ul>
<li>Per-tenant traffic isolation</li>
<li>Use cases involving heavy multitenancy</li>
</ul>
<p><strong>✅ Pros:</strong></p>
<ul>
<li>Traffic isolation at pod level</li>
<li>Dynamic routing at a fine-grained scale</li>
</ul>
<p><strong>⚠️ Cons:</strong></p>
<ul>
<li>Overhead scales with pods</li>
<li>It may not be necessary for most use cases</li>
</ul>
<h3>5. Service Mesh + Gateway (Sidecar Pattern)</h3>
<p><strong>Overview:</strong> Combine an ingress gateway (entry point) with a service mesh (e.g., Istio, Linkerd) that manages service-to-service communication via sidecar proxies.</p>
<p><strong>🧠 Ideal for:</strong></p>
<ul>
<li>Zero-trust environments</li>
<li>Enterprises with complex routing, observability, and policy requirements</li>
</ul>
<p><strong>✅ Pros:</strong></p>
<ul>
<li>Deep observability (metrics, tracing, logging)</li>
<li>Fine-grained traffic management</li>
<li>Built-in mTLS, retries, and circuit breaking</li>
</ul>
<p><strong>⚠️ Cons:</strong></p>
<ul>
<li>High complexity and learning curve</li>
<li>Requires a solid DevOps foundation</li>
</ul>
<h3>🧠 How to Choose the Right Pattern for Your Architecture</h3>
<ol>
<li>Quick MVP or startup ===>Centralized Gateway</li>
<li>Security layering needed===>Tiered Gateway</li>
<li>Independent teams, CI/CD===>Microgateway</li>
<li>Pod-level isolation needed===>Pod-level Gateway</li>
<li>Need deep traffic insights===>Service Mesh + Sidecar</li>
</ol>
<p>Still unsure? A good rule of thumb: <strong>start simple, scale when needed.</strong></p>
<h3>⚙️ Best Practices When Designing API Gateway Architectures</h3>
<p>Here are a few lessons learned from real-world deployments:</p>
<ol>
<li><strong>Avoid business logic in gateways</strong><br>
 Gateways are not mini-apps. Keep logic minimal and offload complex tasks to services.</li>
<li><strong>Secure all entry points</strong><br>
 Use OAuth2, JWT, mTLS, and rate limiting. Don’t leave unprotected APIs in your mesh.</li>
<li><strong>Design for observability</strong><br>
 Collect metrics, logs, and traces right from your gateway layer. It’s your first line of insight.</li>
<li><strong>Embrace API versioning</strong><br>
 Avoid breaking changes. Version your APIs and document them clearly.</li>
<li><strong>Automate with CI/CD</strong><br>
 Treat your API gateway configs (routes, plugins) as code. Use GitOps or IaC for rollout.</li>
</ol>
<h3>✨ Final Thoughts</h3>
<p>The API Gateway isn’t just a reverse proxy anymore — it’s a <strong>strategic control point</strong> in your architecture.</p>
<p>Whether you’re serving millions of users or just building your first microservice, picking the right pattern can dramatically impact performance, scalability, and developer productivity.</p>
<p>If you’re looking to future-proof your architecture, explore modern tools like <strong>Gloo Gateway</strong>, <strong>Kong</strong>, or <strong>Envoy</strong> that support cloud-native standards out of the box.</p>
<hr>
<p>👉 <strong>Was this helpful?</strong> Hit the 💚 or share this with your team.<br>
 Do you have questions or want real-world examples? Let’s chat in the comments!</p>
<p>By Jatin Jain Saraf on April 9, 2025.</p>]]></content:encoded>
      <pubDate>Wed, 09 Apr 2025 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>API</category>
      <category>Backend</category>
      <category>Architecture</category>
    </item>
    <item>
      <title>Handling Large Datasets &amp; High-Traffic Queries: Optimizing Pagination, Sorting &amp; Filtering</title>
      <link>https://insight.jatinjainsaraf.com/handling-large-datasets-high-traffic-queries-optimizing-pagination-sorting-filtering</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/handling-large-datasets-high-traffic-queries-optimizing-pagination-sorting-filtering</guid>
      <description>Performance optimisation becomes a critical challenge when dealing with millions of records in a database.</description>
      <content:encoded><![CDATA[<h1>Handling Large Datasets &#x26; High-Traffic Queries: Optimizing Pagination, Sorting &#x26; Filtering</h1>
<p>Performance optimisation becomes a critical challenge when dealing with millions of records in a database. As datasets grow, queries that…</p>
<hr>
<h3>Handling Large Datasets &#x26; High-Traffic Queries: Optimizing Pagination, Sorting &#x26; Filtering</h3>
<p>Performance optimisation becomes a critical challenge when dealing with millions of records in a database. As datasets grow, queries that once ran smoothly start slowing down, and when multiple users access the system simultaneously, things get even worse.</p>
<h3>The Challenge: Why Do Queries Slow Down?</h3>
<p>Even with proper indexing, large-scale applications often face the following issues:</p>
<ul>
<li><strong>Pagination Performance Drops</strong>: Offset-based pagination (<code>OFFSET n LIMIT m</code>) forces the database to scan and discard rows before returning results, making deeper pages increasingly slower.</li>
<li><strong>Sorting and Filtering Lag</strong>: Queries that require sorting by timestamps, prices, or statuses can still become slow, even when indexed.</li>
<li><strong>Database Overload Due to High-Traffic Queries</strong>: If users frequently access <strong>high-activity data</strong>, like tracking transactions for a busy wallet, caching doesn’t always help because the data is constantly changing.</li>
</ul>
<p>To ensure <strong>fast and scalable</strong> queries, we implemented the following optimizations.</p>
<h3>Optimizing Pagination for Large Datasets</h3>
<h3>Problem:</h3>
<p>Offset-based pagination (<code>OFFSET n LIMIT m</code>) requires the database to <strong>scan and discard</strong> <code>n</code> rows before fetching results. The deeper the page, the worse the performance. This is especially problematic for applications handling real-time blockchain transactions, financial records, or high-traffic APIs.</p>
<h3>Solution: Cursor-Based Pagination</h3>
<p>Instead of using <code>OFFSET</code>, we switched to <strong>cursor-based pagination</strong>, which fetches records <strong>directly from the last seen record</strong>. By using a unique, indexed column (e.g., <code>transaction_id</code> or <code>timestamp</code>), we eliminate unnecessary row scans, making pagination significantly faster.</p>
<p><strong>Why it works:</strong> ✅ Reduces deep pagination lag ✅ Prevents scanning &#x26; discarding large data chunks ✅ Keeps API responses consistently fast</p>
<h3>Enhancing Sorting &#x26; Filtering Performance</h3>
<h3>Problem:</h3>
<p>Sorting and filtering transactions based on fields like <code>timestamp</code>, <code>status</code>, or <code>amount</code> can become slow, even when indexes are applied. Traditional indexes work well for some queries but struggle with efficiently filtering large datasets.</p>
<h3>Solution: Partial Indexes &#x26; Materialized Views</h3>
<ul>
<li><strong>Partial Indexes:</strong> Instead of indexing an entire table, we created <strong>partial indexes</strong> for frequently used filters, such as transactions with <code>status = 'SUCCESS'</code>.</li>
<li><strong>Materialized Views:</strong> For common queries (e.g., “latest transactions”), precomputing results using <strong>materialized views</strong> significantly improved query performance.</li>
</ul>
<p><strong>Why it works:</strong> ✅ Reduces database load for frequent filtering ✅ Improves query execution times by <strong>fetching precomputed data</strong> ✅ Keeps results fresh with periodic updates</p>
<h3>Handling High-Traffic Queries on Frequently Changing Data</h3>
<h3>Problem:</h3>
<p>When users check frequently updated data (like an active wallet address), caching alone isn’t a viable solution. Since the data changes constantly, relying on Redis or Memcached can lead to outdated information.</p>
<h3>Solution: Hybrid Caching + Event-Driven Updates</h3>
<p>To handle this challenge efficiently, we combined multiple strategies:</p>
<ol>
<li><strong>Hybrid Caching Strategy:</strong> Instead of long-term caching, we used <strong>short-lived caches (5–10 seconds)</strong> along with background updates to reduce direct database hits.</li>
<li><strong>Read Replicas:</strong> Queries were offloaded to <strong>read replicas</strong>, ensuring that high-volume reads didn’t overwhelm the primary database.</li>
<li><strong>Event-Driven Updates (CDC — Change Data Capture):</strong> Instead of polling the database repeatedly, we implemented <strong>CDC using Kafka/PostgreSQL logical replication</strong> to <strong>push real-time updates</strong> whenever new transactions were recorded.</li>
<li><strong>Optimized Query Execution:</strong> Instead of fetching <strong>all transactions</strong> for an active wallet, we optimized queries to <strong>fetch only new records since the last request</strong>, reducing dataset size and improving response time.</li>
</ol>
<p><strong>Why it works:</strong> ✅ Reduces database hits while keeping data fresh ✅ Handles <strong>high-traffic wallets with real-time updates</strong> ✅ Ensures <strong>scalability without compromising performance</strong></p>
<h3>Final Outcome &#x26; Key Takeaways</h3>
<p>With these optimizations, we achieved:</p>
<p>✅ <strong>Faster API responses</strong> (up to 80% improvement in query execution time) ✅ <strong>Smooth pagination, sorting &#x26; filtering</strong>, even with millions of records ✅ <strong>Scalable architecture</strong> that handles real-time data efficiently ✅ <strong>Balanced database load</strong>, ensuring high availability under heavy user traffic</p>
<h3>What’s Your Experience?</h3>
<p>Have you faced similar challenges in handling <strong>large datasets with real-time queries</strong>? What worked for you? Let’s discuss your insights in the comments below! 🚀</p>
<p>#DatabaseScaling #Pagination #PerformanceOptimization #HighTrafficApps #BackendEngineering</p>
<p>By Jatin Jain Saraf on February 22, 2025.</p>]]></content:encoded>
      <pubDate>Sat, 22 Feb 2025 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Engineering</category>
    </item>
    <item>
      <title>The Ultimate Git Command Guide: Streamline Your Development Workflow</title>
      <link>https://insight.jatinjainsaraf.com/the-ultimate-git-command-guide-streamline-your-development-workflow</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/the-ultimate-git-command-guide-streamline-your-development-workflow</guid>
      <description>Introduction</description>
      <content:encoded><![CDATA[<h1>The Ultimate Git Command Guide: Streamline Your Development Workflow</h1>
<p>Introduction</p>
<hr>
<h3>The Ultimate Git Command Guide: Streamline Your Development Workflow</h3>
<h3>Introduction</h3>
<p>Git is a vital tool for developers, offering effective version control, teamwork, and project administration. Whether you’re solo or in a team, understanding Git commands is vital for smoothing your development process. This overview details the main Git commands you require, giving you a strong base for handling your code, monitoring alterations, and collaborating with fellow developers. Learning these commands can boost efficiency and maintain project momentum. Here are the essential Git commands every software engineer should master.</p>
<p><strong>Configuration</strong></p>
<blockquote>
<p><strong>1. git config —</strong> Configure Git settings, such as user name and email.<br>
<code>Example: git config --global user.name "Your Name"</code></p>
</blockquote>
<blockquote>
<p><strong>2. git init —</strong> Initialize a new Git repository.<br>
<code>Example: git init</code></p>
</blockquote>
<blockquote>
<p><strong><em>3. git clone</em></strong><code>_Purpose: Clone an existing repository._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git clone_ [_https://github.com/user/repo.git_](https://github.com/user/repo.git)</code></p>
</blockquote>
<blockquote>
<p><strong><em>4. git status</em></strong><code>_Purpose: Show the working directory and staging area status._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git status_</code></p>
</blockquote>
<blockquote>
<p><strong><em>5. git add</em></strong><code>_Purpose: Add file contents to the index (staging area)._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git add . (add all files)_</code></p>
</blockquote>
<blockquote>
<p><strong><em>6. git commit</em></strong><code>_Purpose: Record changes to the repository._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git commit -m "Commit message"_</code></p>
</blockquote>
<blockquote>
<p><strong><em>7. git push</em></strong><code>_Purpose: Update remote refs along with associated objects._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git push origin main_</code></p>
</blockquote>
<blockquote>
<p><strong><em>8. git pull</em></strong><code>_Purpose: Fetch from and integrate with another repository or local branch._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git pull origin main_</code></p>
</blockquote>
<blockquote>
<p><strong><em>9. git branch</em></strong><code>_Purpose: List, create, or delete branches._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git branch new-branch (create new branch)_</code></p>
</blockquote>
<blockquote>
<p><strong><em>10. git checkout</em></strong><code>_Purpose: Switch branches or restore working tree files._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git checkout new-branch (switch to branch)_</code></p>
</blockquote>
<blockquote>
<p><strong><em>11. git switch</em></strong><code>_Purpose: Switch branches._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git switch new-branch_</code></p>
</blockquote>
<blockquote>
<p><strong><em>12. git merge</em></strong><code>_Purpose: Join two or more development histories together._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git merge new-branch (merge new-branch into current branch)_</code></p>
</blockquote>
<blockquote>
<p><strong><em>13. git rebase</em></strong><code>_Purpose: Reapply commits on top of another base tip._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git rebase main_</code></p>
</blockquote>
<blockquote>
<p><strong><em>14. git log</em></strong><code>_Purpose: Show commit logs._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git log --oneline_</code></p>
</blockquote>
<blockquote>
<p><strong><em>15. git diff</em></strong><code>_Purpose: Show changes between commits, commit and working tree, etc._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git diff (show unstaged changes)_</code></p>
</blockquote>
<blockquote>
<p><strong><em>16. git show</em></strong><code>_Purpose: Show various types of objects._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git show HEAD (show changes in the last commit)_</code></p>
</blockquote>
<blockquote>
<p><strong><em>17. git stash</em></strong><code>_Purpose: Stash the changes in a dirty working directory away._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git stash_</code></p>
</blockquote>
<blockquote>
<p><strong><em>18. git stash pop</em></strong><code>_Purpose: Apply the changes recorded in the stash to the working directory._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git stash pop_</code></p>
</blockquote>
<blockquote>
<p><strong><em>19. git clean</em></strong><code>_Purpose: Remove untracked files from the working directory._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git clean -fd_</code></p>
</blockquote>
<blockquote>
<p><strong><em>20. git remote</em></strong><code>_Purpose: Manage set of tracked repositories._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git remote add origin_ [_https://github.com/user/repo.git_](https://github.com/user/repo.git)</code></p>
</blockquote>
<blockquote>
<p><strong><em>21. git fetch</em></strong><code>_Purpose: Download objects and refs from another repository._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git fetch origin_</code></p>
</blockquote>
<blockquote>
<p><strong><em>22. git remote -v</em></strong><code>_Purpose: Show the URLs that a remote name corresponds to._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git remote -v_</code></p>
</blockquote>
<blockquote>
<p><strong><em>23. git tag</em></strong><code>_Purpose: Create, list, delete, or verify a tag object._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git tag -a v1.0 -m "Version 1.0"_</code></p>
</blockquote>
<blockquote>
<p><strong><em>24. git push origin --tags</em></strong><code>_Purpose: Push all tags to the remote repository._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git push origin --tags_</code></p>
</blockquote>
<blockquote>
<p><strong><em>25. git reset</em></strong><code>_Purpose: Reset current HEAD to the specified state._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git reset --hard HEAD~1 (reset to previous commit)_</code></p>
</blockquote>
<blockquote>
<p><strong><em>26. git revert</em></strong><code>_Purpose: Create a new commit that undoes the changes from a previous commit._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git revert HEAD_</code></p>
</blockquote>
<blockquote>
<p><strong><em>27. git checkout --</em></strong><code>_Purpose: Discard changes in the working directory._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git checkout -- file.txt (discard changes in file.txt)_</code></p>
</blockquote>
<blockquote>
<p><strong><em>28. git cherry-pick</em></strong><code>_Purpose: Apply the changes introduced by some existing commits._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git cherry-pick &#x3C;commit-hash>_</code></p>
</blockquote>
<blockquote>
<p><strong><em>29. git branch -d</em></strong><code>_Purpose: Delete a branch._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git branch -d branch-name_</code></p>
</blockquote>
<blockquote>
<p><strong><em>30. git branch -D</em></strong><code>_Purpose: Force delete a branch._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git branch -D branch-name_</code></p>
</blockquote>
<blockquote>
<p><strong><em>31. git merge --no-ff</em></strong><code>_Purpose: Create a merge commit even when the merge resolves as a fast-forward._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git merge --no-ff new-branch_</code></p>
</blockquote>
<blockquote>
<p><strong><em>32. git rebase -i</em></strong><code>_Purpose: Start an interactive rebase._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git rebase -i HEAD~3_</code></p>
</blockquote>
<blockquote>
<p><strong><em>33. git diff --staged</em></strong><code>_Purpose: Show changes between the index and the last commit._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git diff --staged_</code></p>
</blockquote>
<blockquote>
<p><strong>34. git blame</strong></p>
</blockquote>
<blockquote>
<p><code>Purpose: Show what revision and author last modified each line of a file.</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git blame file.txt_</code></p>
</blockquote>
<blockquote>
<p><strong><em>35. git log --graph</em></strong><code>_Purpose: Show a graph of the commit history._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git log --graph --oneline_</code></p>
</blockquote>
<blockquote>
<p><strong><em>36. git reflog</em></strong><code>_Purpose: Show a log of all references._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git reflog_</code></p>
</blockquote>
<blockquote>
<p><strong><em>37. git stash list</em></strong><code>_Purpose: List all stashes._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git stash list_</code></p>
</blockquote>
<blockquote>
<p><strong><em>38. git stash apply</em></strong><code>_Purpose: Apply a stash to the working directory._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git stash apply stash@{1}_</code></p>
</blockquote>
<blockquote>
<p><strong><em>39. git stash drop</em></strong><code>_Purpose: Remove a single stash entry from the list of stashes._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git stash drop stash@{1}_</code></p>
</blockquote>
<blockquote>
<p><strong><em>40. git remote show</em></strong><code>_Purpose: Show information about the remote repository._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git remote show origin_</code></p>
</blockquote>
<blockquote>
<p><strong><em>41. git remote rm</em></strong><code>_Purpose: Remove a remote._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git remote rm origin_</code></p>
</blockquote>
<blockquote>
<p><strong><em>42. git pull --rebase</em></strong><code>_Purpose: Fetch and rebase the current branch on top of the upstream branch._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git pull --rebase origin main_</code></p>
</blockquote>
<blockquote>
<p><strong>43. git fetch --all</strong></p>
</blockquote>
<blockquote>
<p><code>Purpose: Fetch all remotes.</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git fetch --all_</code></p>
</blockquote>
<blockquote>
<p><strong><em>44. git bisect</em></strong><code>_Purpose: Use binary search to find the commit that introduced a bug._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git bisect start_</code></p>
</blockquote>
<blockquote>
<p><strong><em>45. git submodule</em></strong><code>_Purpose: Initialize, update, or inspect submodules._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git submodule update --init_</code></p>
</blockquote>
<blockquote>
<p><strong><em>46. git archive</em></strong><code>_Purpose: Create an archive of files from a named tree._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git archive --format=tar HEAD > archive.tar_</code></p>
</blockquote>
<blockquote>
<p><strong><em>47. git shortlog</em></strong><code>_Purpose: Summarize git log output._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git shortlog -s -n_</code></p>
</blockquote>
<blockquote>
<p><strong>48. git describe</strong></p>
</blockquote>
<blockquote>
<p><code>Purpose: Give an object a human-readable name based on an available ref.</code></p>
</blockquote>
<blockquote>
<p>Example: git describe --tags</p>
</blockquote>
<blockquote>
<p><strong><em>49. git rev-parse</em></strong><code>_Purpose: Parse revision (or other objects) and retrieve its hash._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git rev-parse HEAD_</code></p>
</blockquote>
<blockquote>
<p><strong><em>50. git tag -d</em></strong><code>_Purpose: Delete a tag from the local repository._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git tag -d v1.0_</code></p>
</blockquote>
<blockquote>
<p><strong><em>51. git checkout -b</em></strong><code>_Purpose: Create and switch to a new branch._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git checkout -b new-branch_</code></p>
</blockquote>
<blockquote>
<p><strong><em>52. git push origin --delete</em></strong><code>_Purpose: Delete a remote branch._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git push origin --delete branch-name_</code></p>
</blockquote>
<blockquote>
<p><strong><em>53. git cherry</em></strong><code>_Purpose: Find commits not merged upstream._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git cherry -v_</code></p>
</blockquote>
<blockquote>
<p><strong><em>54. git rm</em></strong><code>_Purpose: Remove files from the working tree and from the index._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git rm file.txt_</code></p>
</blockquote>
<blockquote>
<p><strong><em>55. git mv</em></strong><code>_Purpose: Move or rename a file, directory, or symlink._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git mv oldname.txt newname.txt_</code></p>
</blockquote>
<blockquote>
<p><strong><em>56. git reset HEAD</em></strong><code>_Purpose: Unstage changes._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git reset HEAD file.txt_</code></p>
</blockquote>
<blockquote>
<p><strong><em>57. git log -p</em></strong><code>_Purpose: Show changes over time for a specific file._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git log -p file.txt_</code></p>
</blockquote>
<blockquote>
<p><strong><em>58. git diff --cached</em></strong><code>_Purpose: Show changes between the index and the last commit (same as --staged)._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git diff --cached_</code></p>
</blockquote>
<blockquote>
<p><strong><em>59. git apply</em></strong><code>_Purpose: Apply a patch to files and/or to the index._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git apply patch.diff_</code></p>
</blockquote>
<blockquote>
<p><strong><em>60. git format-patch</em></strong><code>_Purpose: Prepare patches for e-mail submission._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git format-patch -1 HEAD_</code></p>
</blockquote>
<blockquote>
<p><strong><em>61. git am</em></strong><code>_Purpose: Apply a series of patches from a mailbox._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git am &#x3C; patch.mbox_</code></p>
</blockquote>
<blockquote>
<p><strong><em>62. git cherry-pick --continue</em></strong><code>_Purpose: Resume cherry-picking after resolving conflicts._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git cherry-pick --continue_</code></p>
</blockquote>
<blockquote>
<p><strong><em>63. git fsck</em></strong><code>_Purpose: Verify the connectivity and validity of objects in the database._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git fsck_</code></p>
</blockquote>
<blockquote>
<p><strong><em>64. git gc</em></strong><code>_Purpose: Cleanup unnecessary files and optimize the local repository._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git gc_</code></p>
</blockquote>
<blockquote>
<p><strong><em>65. git prune</em></strong><code>_Purpose: Remove unreachable objects from the object database._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git prune_</code></p>
</blockquote>
<blockquote>
<p><strong><em>66. git notes</em></strong><code>_Purpose: Add or inspect object notes._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git notes add -m "Note message"_</code></p>
</blockquote>
<blockquote>
<p><strong><em>67. git whatchanged</em></strong><code>_Purpose: Show what changed, similar to git log._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git whatchanged_</code></p>
</blockquote>
<blockquote>
<p><strong>68. git show-branch</strong><code>_Purpose: Show branches and their commits._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git show-branch_</code></p>
</blockquote>
<blockquote>
<p><strong><em>69. git verify-tag</em></strong><code>_Purpose: Check the GPG signature of tags._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git verify-tag v1.0_</code></p>
</blockquote>
<blockquote>
<p><strong><em>70. git show-ref</em></strong><code>_Purpose: List references in a local repository._</code></p>
</blockquote>
<blockquote>
<p><code>_Example: git show-ref_</code></p>
</blockquote>
<p><code>Twitter Account</code>: <a href="https://twitter.com/JatinJainSaraf1">Twitter</a></p>
<p>By Jatin Jain Saraf on July 9, 2024.</p>]]></content:encoded>
      <pubDate>Tue, 09 Jul 2024 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Git</category>
      <category>Development Tools</category>
    </item>
    <item>
      <title>Strings in JavaScript: An In-Depth Guide</title>
      <link>https://insight.jatinjainsaraf.com/strings-in-javascript-an-in-depth-guide</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/strings-in-javascript-an-in-depth-guide</guid>
      <description>Strings are a fundamental part of programming in JavaScript, representing textual data. In this guide, we will explore the intricacies of…</description>
      <content:encoded><![CDATA[<h1>Strings in JavaScript: An In-Depth Guide</h1>
<p>Strings are a fundamental part of programming in JavaScript, representing textual data. In this guide, we will explore the intricacies of…</p>
<hr>
<h3>Strings in JavaScript: An In-Depth Guide</h3>
<p>Strings are a fundamental part of programming in JavaScript, representing textual data. In this guide, we will explore the intricacies of strings in JavaScript, including their creation, manipulation, and the top 50 methods you can use to handle them effectively.</p>
<p><strong>Introduction to Strings</strong></p>
<p>In JavaScript, a string is a sequence of characters used to represent text. They are immutable, meaning once created, their values cannot be changed.</p>
<p><strong>String Creation</strong></p>
<p>You can create strings in JavaScript using single quotes (`’`), double quotes (`”`), or backticks (`` ` ``) for template literals.</p>
<p>let singleQuote = ‘Hello, world!’;<br>
let doubleQuote = “Hello, world!”;<br>
let templateLiteral = `Hello, world!`;</p>
<p><strong>String Properties</strong></p>
<ol>
<li><strong>`length`</strong><br>
The `length` property returns the number of characters in a string.</li>
</ol>
<p>let text = “Hello”;<br>
console.log(text.length); // Output: 5</p>
<p>2. `<strong>charAt()</strong>`<br>
Returns the character at a specified index.</p>
<p>let text = “Hello”;<br>
console.log(text.charAt(0)); // Output: H</p>
<p>3. <strong>`charCodeAt()`</strong><br>
Returns the Unicode of the character at a specified index.</p>
<p>let text = “Hello”;<br>
console.log(text.charCodeAt(0)); // Output: 72</p>
<p>4. <strong>`concat()`</strong><br>
Joins two or more strings.</p>
<p>let text1 = “Hello”;<br>
let text2 = “World”;<br>
console.log(text1.concat(“ “, text2)); // Output: Hello World</p>
<p>5. <strong>`includes()`</strong><br>
Checks if a string contains a specified value.</p>
<p>let text = “Hello World”;<br>
console.log(text.includes(“World”)); // Output: true</p>
<p>6. <strong>`endsWith()`</strong><br>
Checks if a string ends with a specified value.</p>
<p>let text = “Hello World”;<br>
console.log(text.endsWith(“World”)); // Output: true</p>
<ol start="7">
<li><strong>`indexOf()`</strong><br>
Returns the index of the first occurrence of a specified value.</li>
</ol>
<p>let text = “Hello World”;<br>
console.log(text.indexOf(“World”)); // Output: 6</p>
<p>8. <strong>`lastIndexOf()`</strong><br>
Returns the index of the last occurrence of a specified value.</p>
<p>let text = “Hello World World”;<br>
console.log(text.lastIndexOf(“World”)); // Output: 12</p>
<p>9. <strong>`match()`</strong><br>
Searches a string for a match against a regular expression.</p>
<p>let text = “Hello World”;<br>
console.log(text.match(/World/)); // Output: [“World”]</p>
<p>10. <strong>`matchAll()`</strong><br>
Returns an iterator of all results matching a string against a regular expression.</p>
<p>let text = “test1test2”;<br>
const matches = text.matchAll(/t(e)(st(\d?))/g);<br>
for (const match of matches) {<br>
console.log(match);<br>
}<br>
// Output:<br>
// [“test1”, “e”, “st1”, “1”]<br>
// [“test2”, “e”, “st2”, “2”]</p>
<p>11. <strong>`padEnd()`</strong><br>
Pads the current string with another string until the resulting string reaches the given length.</p>
<p>let text = “Hello”;<br>
console.log(text.padEnd(10, “*”)); // Output: Hello*****</p>
<p>12. <strong>`padStart()`</strong><br>
Pads the current string with another string until the resulting string reaches the given length.</p>
<p>let text = “Hello”;<br>
console.log(text.padStart(10, “*”)); // Output: *****Hello</p>
<p>13. <strong>`repeat()`</strong><br>
Returns a new string with a specified number of copies of an existing string.</p>
<p>let text = “Hello”;<br>
console.log(text.repeat(3)); // Output: HelloHelloHello</p>
<p>14. <strong>`replace()`</strong><br>
Searches for a match between a regular expression and a string, and replaces the matched substring with a new substring.</p>
<p>let text = “Hello World”;<br>
console.log(text.replace(“World”, “Everyone”)); // Output: Hello Everyone</p>
<p>15. <strong>`replaceAll()`</strong><br>
Returns a new string with all matches of a pattern replaced by a replacement.</p>
<p>let text = “Hello World World”;<br>
console.log(text.replaceAll(“World”, “Everyone”)); // Output: Hello Everyone Everyone</p>
<ol start="16">
<li><strong>`search()`</strong><br>
Executes a search for a match between a regular expression and a specified string.</li>
</ol>
<p>let text = “Hello World”;<br>
console.log(text.search(“World”)); // Output: 6</p>
<p>17. <strong>`slice()`</strong><br>
Extracts a section of a string and returns it as a new string.</p>
<p>let text = “Hello World”;<br>
console.log(text.slice(0, 5)); // Output: Hello</p>
<p>18. <strong>`split()`</strong><br>
Splits a string into an array of substrings.</p>
<p>let text = “Hello World”;<br>
console.log(text.split(“ “)); // Output: [“Hello”, “World”]</p>
<p>19. <strong>`startsWith()`</strong><br>
Checks if a string starts with a specified value.</p>
<p>let text = “Hello World”;<br>
console.log(text.startsWith(“Hello”)); // Output: true</p>
<p>20. <strong>`substring()`</strong><br>
Returns a subset of a string between one index and another.</p>
<p>let text = “Hello World”;<br>
console.log(text.substring(0, 5)); // Output: Hello</p>
<p>21. <strong>`toLowerCase()`</strong><br>
Converts a string to lowercase letters.</p>
<p>let text = “Hello World”;<br>
console.log(text.toLowerCase()); // Output: hello world</p>
<ol start="22">
<li><strong>`toUpperCase()`</strong><br>
Converts a string to uppercase letters.</li>
</ol>
<p>let text = “Hello World”;<br>
console.log(text.toUpperCase()); // Output: HELLO WORLD</p>
<ol start="23">
<li><strong>`trim()`</strong><br>
Removes whitespace from both ends of a string.</li>
</ol>
<p>let text = “ Hello World “;<br>
console.log(text.trim()); // Output: Hello World</p>
<p>24. <strong>`trimEnd()`</strong><br>
Removes whitespace from the end of a string.</p>
<p>let text = “ Hello World “;<br>
console.log(text.trimEnd()); // Output: “ Hello World”</p>
<p>25. <strong>`trimStart()`</strong></p>
<p>Removes whitespace from the start of a string.</p>
<p>let text = “ Hello World “;<br>
console.log(text.trimStart()); // Output: “Hello World “</p>
<p>26. <strong>`valueOf()`</strong><br>
Returns the primitive value of a string object.</p>
<p>let text = new String(“Hello World”);<br>
console.log(text.valueOf()); // Output: “Hello World”</p>
<p>27. <strong>`codePointAt()`</strong><br>
Returns a non-negative integer that is the Unicode code point value.</p>
<p>let text = “Hello”;<br>
console.log(text.codePointAt(0)); // Output: 72</p>
<p>28. <strong>`fromCharCode()`</strong><br>
Creates a string from the specified sequence of UTF-16 code units.</p>
<p>console.log(String.fromCharCode(72, 101, 108, 108, 111)); // Output: “Hello”</p>
<p>29. <strong>`fromCodePoint()`</strong><br>
Creates a string from the specified sequence of code points.</p>
<p>console.log(String.fromCodePoint(128512)); // Output: “😀”</p>
<p>30. <strong>`raw()`</strong><br>
Returns a raw string from a template string.</p>
<p>console.log(String.raw`Hello\nWorld`); // Output: “Hello\\nWorld”</p>
<p>31. <strong>`localeCompare()`</strong><br>
Compares two strings in the current locale.</p>
<p>let text1 = “a”;<br>
let text2 = “b”;<br>
console.log(text1.localeCompare(text2)); // Output: -1</p>
<ol start="32">
<li><strong>`normalize()`</strong><br>
Returns the Unicode Normalization Form of the string.</li>
</ol>
<p>let text = “\u004F\u030C”;<br>
console.log(text.normalize()); // Output: “Ǒ”</p>
<ol start="33">
<li><strong>`toString()`</strong><br>
Returns a string representing the specified object.</li>
</ol>
<p>let text = new String(“Hello”);<br>
console.log(text.toString()); // Output: “Hello”</p>
<p>34. <strong>`toLocaleLowerCase()`</strong><br>
Converts a string to lowercase letters, according to the host’s current locale.</p>
<p>let text = “Hello World”;<br>
console.log(text.toLocaleLowerCase()); // Output: hello world</p>
<p>35. <strong>`toLocaleUpperCase()`</strong><br>
Converts a string to uppercase letters, according to the host’s current locale.</p>
<p>let text = “Hello World”;<br>
console.log(text.toLocaleUpperCase()); // Output: HELLO WORLD</p>
<p>36. **`anchor()`<br>
**Creates an HTML anchor.</p>
<p>let text = “Hello”;<br>
console.log(text.anchor(“myanchor”)); // Output: <a name="user-content-”myanchor”">Hello</a></p>
<p>37. <strong>`big()`</strong><br>
Creates a string to be displayed in a big font.</p>
<p>let text = “Hello”;<br>
console.log(text.big()); // Output: Hello</p>
<p>38. <strong>`blink()`</strong><br>
Creates a string to be displayed as blinking text.</p>
<p>let text = “Hello”;<br>
console.log(text.blink()); // Output: Hello</p>
<p>39. <strong>`bold()`</strong><br>
Creates a string to be displayed as bold.</p>
<p>let text = “Hello”;<br>
console.log(text.bold()); // Output: <b>Hello</b></p>
<p>40. <strong>`fixed()`</strong><br>
Creates a string to be displayed in a fixed-pitch font.</p>
<p>let text = “Hello”;<br>
console.log(text.fixed()); // Output: <tt>Hello</tt></p>
<p>41. <strong>`fontcolor()`</strong><br>
Creates a string to be displayed in a specified color.</p>
<p>let text = “Hello”;<br>
console.log(text.fontcolor(“red”)); // Output: Hello</p>
<p>42. <strong>`fontsize()`</strong><br>
Creates a string to be displayed in a specified size.</p>
<p>let text = “Hello”;<br>
console.log(text.fontsize(7)); // Output: &#x3C;font size=”7">Hello</p>
<p>43. <strong>`italics()`</strong><br>
Creates a string to be displayed as italic.</p>
<p>let text = “Hello”;<br>
console.log(text.italics()); // Output: <i>Hello</i></p>
<p>44. <strong>`link()`</strong><br>
Creates a string to be displayed as a hyperlink.</p>
<p>let text = “Hello”;<br>
console.log(text.link(“<a href="https://www.example.com">https://www.example.com</a>")); // Output: &#x3C;a href=”<a href="https://www.example.com%22%5C%3EHello">https://www.example.com"\>Hello</a></p>
<p>45. <strong>`small()`</strong><br>
Creates a string to be displayed in a small font.</p>
<p>let text = “Hello”;<br>
console.log(text.small()); // Output: Hello</p>
<p>46. <strong>`strike()`</strong><br>
Creates a string to be displayed with a strikethrough.</p>
<p>let text = “Hello”;<br>
console.log(text.strike()); // Output: <strike>Hello</strike></p>
<p>47. <strong>`sub()`</strong><br>
Creates a string to be displayed as subscript.</p>
<p>let text = “Hello”;<br>
console.log(text.sub()); // Output: &#x3C;sub>Hello&#x3C;/sub></p>
<p>48. <strong>`sup()`</strong><br>
Creates a string to be displayed as superscript.</p>
<p>let text = “Hello”;<br>
console.log(text.sup()); // Output: <sup>Hello</sup></p>
<ol start="49">
<li><strong>`trimLeft()`</strong><br>
Removes whitespace from the start of a string.</li>
</ol>
<p>let text = “ Hello World “;<br>
console.log(text.trimLeft()); // Output: “Hello World “</p>
<p>50. <strong>`trimRight()`</strong><br>
Removes whitespace from the end of a string.</p>
<p>let text = “ Hello World “;<br>
console.log(text.trimRight()); // Output: “ Hello World”</p>
<p><strong>Conclusion</strong></p>
<p>Strings are an essential part of JavaScript, and knowing how to manipulate them effectively is crucial for any developer. This guide covered the basics of strings and provided a comprehensive list of methods to work with them. Whether you’re searching, replacing, or formatting text, these methods will help you handle strings efficiently in your JavaScript projects.</p>
<p>By Jatin Jain Saraf on June 21, 2024.</p>]]></content:encoded>
      <pubDate>Fri, 21 Jun 2024 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>JavaScript</category>
      <category>Frontend</category>
    </item>
    <item>
      <title>Choosing Between Type and Interface in TypeScript: A Detailed Guide</title>
      <link>https://insight.jatinjainsaraf.com/choosing-between-type-and-interface-in-typescript-a-detailed-guide</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/choosing-between-type-and-interface-in-typescript-a-detailed-guide</guid>
      <description>When working with TypeScript in Node.js, you often need to define the shape of objects. This is where TypeScript’s type and inter</description>
      <content:encoded><![CDATA[<h1>Choosing Between Type and Interface in TypeScript: A Detailed Guide</h1>
<p>When working with TypeScript in Node.js, you often need to define the shape of objects. This is where TypeScript’s type and interface come…</p>
<hr>
<h3>Choosing Between Type and Interface in TypeScript: A Detailed Guide</h3>
<p>When working with TypeScript in Node.js, you often need to define the shape of objects. This is where TypeScript’s <code>type</code> and <code>interface</code> come into play. Both are used to describe the structure of an object, but choosing between them depends on several factors. Here’s a detailed guide to help you make an informed decision.</p>
<h4>1. Usage Intentions</h4>
<ul>
<li><strong>Interfaces</strong>: Best for defining the structure of objects and classes. They serve as contracts that ensure a class or object adheres to a particular shape.</li>
<li><strong>Types</strong>: Ideal for creating aliases for primitive types, union types, tuple types, and more complex type expressions.</li>
</ul>
<h4>2. Extensibility</h4>
<ul>
<li><strong>Interfaces</strong>: Can be extended using the <code>extends</code> keyword. This feature supports hierarchical and flexible designs, making it easy to create new interfaces that build upon existing ones.</li>
<li><strong>Types</strong>: Can be extended using intersection types (<code>&#x26;</code>), which combine multiple types into one.</li>
</ul>
<p>interface User {<br>
id: number;<br>
username: string;<br>
email: string;<br>
}</p>
<p>interface Admin extends User {<br>
adminLevel: number;<br>
}</p>
<p>type User = {<br>
id: number;<br>
username: string;<br>
email: string;<br>
};</p>
<p>type Admin = User &#x26; {<br>
adminLevel: number;<br>
};</p>
<h4>3. Declaration Merging</h4>
<ul>
<li><strong>Interfaces</strong>: Support declaration merging, which means you can define the same interface multiple times, and TypeScript will merge them into a single definition.</li>
<li><strong>Types</strong>: Do not support declaration merging. Attempting to redefine a type alias will result in an error.</li>
</ul>
<p>interface User {<br>
id: number;<br>
username: string;<br>
}</p>
<p>interface User {<br>
email: string;<br>
}</p>
<p>// Merged User interface: { id: number; username: string; email: string; }</p>
<p>type User = {<br>
id: number;<br>
username: string;<br>
};</p>
<p>type User = {<br>
email: string;<br>
}; // Error: Duplicate identifier 'User'</p>
<h4>4. Complex Types</h4>
<ul>
<li><strong>Types</strong>: More powerful for defining complex types such as union types, intersection types, or tuples.</li>
</ul>
<p>type User = {<br>
id: number;<br>
username: string;<br>
email: string;<br>
};</p>
<p>type ApiResponse = User | { error: string };</p>
<h3>Practical Examples</h3>
<ul>
<li><strong>Interface Example</strong>:</li>
</ul>
<p>interface User {<br>
id: number;<br>
username: string;<br>
email: string;<br>
}</p>
<p>function getUserById(id: number): User {<br>
return { id, username: "john_doe", email: "<a href="mailto:john@example.com">john@example.com</a>" };<br>
}</p>
<p><strong>Type Example</strong>:</p>
<p>type User = {<br>
id: number;<br>
username: string;<br>
email: string;<br>
};</p>
<p>function getUserById(id: number): User {<br>
return { id, username: "john_doe", email: "<a href="mailto:john@example.com">john@example.com</a>" };<br>
}</p>
<h3>When to Use Each</h3>
<p><strong>Use</strong> <code>**interface**</code> <strong>when</strong>:</p>
<ul>
<li>Defining the shape of an object or class.</li>
<li>Expecting the type to be extended or implemented by other types.</li>
<li>Benefiting from declaration merging.</li>
</ul>
<p><strong>Use</strong> <code>**type**</code> <strong>when</strong>:</p>
<ul>
<li>Defining complex types (e.g., union, intersection, tuple).</li>
<li>Creating type aliases for primitives, union, and intersection types.</li>
<li>Utilising type inference extensively.</li>
</ul>
<h3>Conclusion</h3>
<p>Both <code>type</code> and <code>interface</code> are essential tools in TypeScript, each with its unique strengths. Interfaces are excellent for defining object shapes and supporting extensibility and declaration merging. Types excel at creating complex types and leveraging TypeScript’s powerful type inference. Understanding these distinctions will help you write more robust, maintainable, and scalable TypeScript code.</p>
<p>Choose wisely based on your specific use case, and don’t hesitate to use both where appropriate to take full advantage of TypeScript’s type system. Happy coding!</p>
<p>By Jatin Jain Saraf on June 19, 2024.</p>]]></content:encoded>
      <pubDate>Wed, 19 Jun 2024 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>TypeScript</category>
      <category>Frontend</category>
    </item>
    <item>
      <title>Unveiling the Power of Descriptive Naming Conventions in JavaScript</title>
      <link>https://insight.jatinjainsaraf.com/unveiling-the-power-of-descriptive-naming-conventions-in-javascript</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/unveiling-the-power-of-descriptive-naming-conventions-in-javascript</guid>
      <description>Introduction:</description>
      <content:encoded><![CDATA[<h1>Unveiling the Power of Descriptive Naming Conventions in JavaScript</h1>
<p>Introduction:</p>
<hr>
<h3>Unveiling the Power of Descriptive Naming Conventions in JavaScript</h3>
<p><strong>Introduction:</strong></p>
<p>In the world of programming, clarity and readability are paramount. Among the many tools at a developer’s disposal, one often overlooked but potent technique is the art of naming variables descriptively. In JavaScript, where agility and efficiency are prized, the choice of variable names can significantly impact code comprehension and maintainability. Let’s delve into the profound impact of descriptive naming conventions and explore the diverse perspectives developers encounter when crafting variable names in JavaScript.</p>
<p>**Why Descriptive Naming Matters:<br>
**Clear and descriptive variable names serve as a form of self-documentation within the codebase. They act as signposts, guiding fellow developers through the logic and functionality encapsulated within each variable. Imagine stumbling upon a cryptically named variable like <code>x</code> or <code>temp</code>. Deciphering its purpose requires extra mental effort and potentially interrupts the flow of understanding. Conversely, encountering a well-named variable such as <code>userProfile</code> or <code>totalSales</code> instantly communicates its role, reducing cognitive overhead and enhancing code comprehension.</p>
<p>Furthermore, descriptive naming fosters code maintainability by reducing the need for extraneous comments or documentation. Instead of relying solely on inline explanations, meaningful variable names convey intent and context, making the code more self-explanatory. This not only streamlines the development process but also facilitates smoother collaboration among team members, as everyone can quickly grasp the purpose and function of each variable.</p>
<p>**The Journey of Naming Variables:<br>
**Naming variables in JavaScript is more than a mechanical task; it’s a journey that encapsulates various perspectives and considerations. Developers often tread the fine line between brevity and clarity, striving to strike a balance that ensures concise yet descriptive names. This balance becomes particularly crucial in the fast-paced environment of JavaScript development, where efficiency is key.</p>
<p>Consider the different perspectives developers adopt when naming variables. Some may prioritize brevity, opting for succinct names like <code>num</code> for "number" or <code>str</code> for "string." While these names save keystrokes, they sacrifice clarity and may lead to confusion, especially in larger codebases. Others lean towards verbosity, choosing names that explicitly spell out the variable's purpose, such as <code>customerFirstName</code> instead of <code>fname</code>. While descriptive, excessively long names can clutter the code and hinder readability.</p>
<p>Navigating these perspectives requires an understanding of the underlying trade-offs. Abbreviated names offer brevity but risk ambiguity, while verbose names provide clarity but may introduce noise. Finding the sweet spot often involves considering factors such as the variable’s scope, lifespan, and the broader context of the codebase.</p>
<p>**Best Practices for Descriptive Naming:<br>
**To harness the full power of descriptive naming in JavaScript, developers should adhere to a set of best practices:</p>
<ol>
<li>Prioritize Clarity: Choose names that convey the purpose and usage of the variable. Aim for names that are self-explanatory and require minimal additional context to understand.</li>
<li>Use Meaningful Context: Incorporate relevant context into variable names, such as function or scope. Prefixes like <code>is</code> for booleans or <code>get</code> for getter functions can provide valuable hints about a variable's behaviour.</li>
<li>Be Consistent: Establish and adhere to consistent naming conventions across projects and teams. Consistency fosters predictability and reduces the cognitive load when navigating different codebases.</li>
<li>Avoid Ambiguity: Steer clear of ambiguous or misleading names that can lead to confusion. Choose descriptive terms that accurately reflect the variable’s purpose and avoid generic placeholders like <code>data</code> or <code>value</code>.</li>
</ol>
<p><strong>Case Studies and Examples:</strong> Let’s examine a few real-world examples to illustrate the impact of descriptive naming on code readability:</p>
<p><strong>Example 1: Poorly Named Variable</strong></p>
<p>// Poorly named variable<br>
let a = 10;<br>
// Requiring additional context<br>
if (a > 5) {<br>
// What does "a" represent?<br>
console.log("Greater than 5");<br>
}</p>
<p><strong>Example 2: Well-Named Variable</strong></p>
<p>// Well-named variable<br>
let numberOfStudents = 10;<br>
// Clear and self-explanatory<br>
if (numberOfStudents > 5) {<br>
console.log("Class size is adequate");<br>
}</p>
<p>In the first example, the variable <code>a</code> provides little insight into its purpose, requiring additional mental effort to decipher its meaning within the code. Conversely, the second example uses a descriptive name <code>numberOfStudents</code>, instantly clarifying its role and eliminating ambiguity.</p>
<p><strong>Navigating Common Pitfalls:</strong><br>
While embracing descriptive naming conventions, developers should be wary of common pitfalls:</p>
<ol>
<li>Overly Verbose Names: Avoid excessively long variable names that add unnecessary verbosity to the code without providing significant clarity.</li>
<li>Excessive Abbreviations: While brevity is essential, excessive abbreviations can obscure the meaning of variable names. Strike a balance between brevity and clarity.</li>
<li>Lack of Consistency: Inconsistent naming conventions within a codebase can lead to confusion. Establish clear guidelines and ensure consistency across all variables.</li>
</ol>
<p><strong>Conclusion</strong><br>
Descriptive naming conventions are a cornerstone of effective JavaScript development, enhancing code readability, comprehension, and maintainability. By prioritizing clarity and context in variable names, developers can streamline collaboration, reduce cognitive overhead, and foster more robust codebases. Embrace the power of descriptive naming, and unlock the full potential of your JavaScript projects.</p>
<p>**Call to Action<br>
**Share your experiences and insights into naming variables in JavaScript. How do you approach the challenge of crafting descriptive names? Join the conversation and contribute to the collective knowledge of the developer community.</p>
<p>By Jatin Jain Saraf on May 29, 2024.</p>]]></content:encoded>
      <pubDate>Wed, 29 May 2024 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>JavaScript</category>
      <category>Frontend</category>
    </item>
    <item>
      <title>Navigating Your Career: Deciding Which Skills to Develop at Each Stage</title>
      <link>https://insight.jatinjainsaraf.com/navigating-your-career-deciding-which-skills-to-develop-at-each-stage</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/navigating-your-career-deciding-which-skills-to-develop-at-each-stage</guid>
      <description>As a Full-stack developer with 4 years of experience, I’ve learned firsthand the importance of strategic skill development at</description>
      <content:encoded><![CDATA[<h1>Navigating Your Career: Deciding Which Skills to Develop at Each Stage</h1>
<p>As a Full-stack developer with 4 years of experience, I’ve learned firsthand the importance of strategic skill development at every stage…</p>
<hr>
<h3>Navigating Your Career: Deciding Which Skills to Develop at Each Stage</h3>
<p>As a Full-stack developer with 4 years of experience, I’ve learned firsthand the importance of strategic skill development at every stage of my career journey. Deciding which skills to cultivate can significantly impact your professional growth and trajectory. Here’s how I approach this critical decision-making process.</p>
<h3><strong>Setting a Career Roadmap</strong></h3>
<p>Before diving into skill development, it’s crucial to map out your career trajectory. Identify your long-term goals — whether it’s becoming a technical architect, leading a development team, or starting your venture. Understanding where you want to be in 5 or 10 years helps in charting a path for skill acquisition.</p>
<h3><strong>Assessing Current Skills</strong></h3>
<p>Evaluate your existing skill set. As a Full Stack developer, you likely possess proficiency in web development technologies, frameworks, databases, and possibly some soft skills like problem-solving and communication. Recognize your strengths and areas for improvement.</p>
<h3><strong>Understanding Career Stages</strong></h3>
<p>Career progression can be broadly categorized into stages — entry-level, mid-level, and senior/expert level. Each stage demands a specific skill set:</p>
<h4><strong>Entry-Level (0–2 years):</strong></h4>
<p>Focus on foundational technical skills (e.g., programming languages, version control, web frameworks).<br>
Develop collaboration and basic project management skills.</p>
<h4><strong>Mid-Level (2–5 years):</strong></h4>
<p>Deepen technical expertise in specific domains (e.g., front-end, back-end, databases).<br>
Enhance leadership and mentoring capabilities by taking on more responsibilities.</p>
<h4><strong>Senior/Expert Level (5+ years):</strong></h4>
<p>Master advanced technologies and architectures (e.g., cloud computing, microservices).<br>
Cultivate strategic thinking, decision-making, and team leadership skills.</p>
<h3><strong>Identifying Industry Trends</strong></h3>
<p>Stay abreast of industry trends and emerging technologies. Research which skills are in demand and align with your career goals. For instance, with the rise of AI and machine learning, understanding these concepts can be advantageous for future opportunities.</p>
<h3>Considering Personal Interests</h3>
<p>Factor in your passions and interests when choosing skills to develop. Enjoy working with data? Consider diving deeper into data analytics or data science. Prefer creating seamless user experiences? Sharpen your front-end development skills.</p>
<h3>Seeking Feedback and Mentorship</h3>
<p>Engage with mentors or colleagues who have navigated similar career paths. Seek feedback on your skill development plan and identify blind spots or areas that might need attention.</p>
<h3>Continual Learning and Adaptation</h3>
<p>Lastly, remember that the tech landscape evolves rapidly. Continual learning is paramount. Allocate time for self-study, online courses, workshops, and conferences to keep your skills sharp and relevant.</p>
<h3>Conclusion</h3>
<p>Deciding which skills to develop at each career stage requires a blend of foresight, self-awareness, and adaptability. By strategically investing in the right skills, you pave the way for sustained professional growth, increased job satisfaction, and the fulfilment of your career aspirations. Remember, your skills are the building blocks of your career — choose wisely and embark confidently on your path to excellence.</p>
<p><strong>Chart your course. Develop with purpose. Excel in every stage. 🚀</strong></p>
<p><strong>#TechCareer #SkillDevelopment #FullStackDeveloper #ProfessionalGrowth #CareerDevelopment #TechSkills #ContinuousLearning #WebDevelopment #SoftwareEngineering</strong></p>
<p>By Jatin Jain Saraf on April 26, 2024.</p>]]></content:encoded>
      <pubDate>Fri, 26 Apr 2024 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Career</category>
      <category>Growth</category>
    </item>
    <item>
      <title>What&apos;s New in Next.js 14</title>
      <link>https://insight.jatinjainsaraf.com/nextjs-14-release-overview</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/nextjs-14-release-overview</guid>
      <description>Next Js 14 The most recent version of the popular React Framework for building and developing web applications, Next.js 14, was released on October…</description>
      <content:encoded><![CDATA[<h1>Next Js 14</h1>
<p>The most recent version of the popular React Framework for building and developing web applications, Next.js 14, was released on October…</p>
<hr>
<h3>Next Js 14</h3>
<p>The most recent version of the popular React Framework for building and developing web applications, Next.js 14, was released on October 26th, 2023. It promises to simplify and improve development speed, making it an excellent choice for any developer who wants to begin building fast and scalable web applications.</p>
<p><strong>Before moving forward with the next 14 one should know about react</strong></p>
<p>What do nextjs do</p>
<p>Next.js 14 includes several updates and improvements that will make the development of web applications a lot faster, let’s look at some of the important updates and improvements:</p>
<h3>Turbopack</h3>
<p>Turbopack, an exciting feature introduced in Next.js 14, represents a significant leap forward in enhancing local development speed. Building on a continuous improvement journey that began with Next.js 13, it has been fine-tuned for optimizing local development in both Pages and the App Router.</p>
<p>The Rust-based compiler is on the brink of stability, with a renewed focus on ensuring compatibility with all Next.js features.</p>
<p>Turbopack has powered 5,000 integration tests for the Next.js devs, which span seven years of bug fixes and reproductions. While on <a href="http://vercel.com/">vercel.com</a>, the results have been remarkable:</p>
<ul>
<li>Local server startup speeds have surged by up to 53.3%.</li>
<li>Fast Refresh now delivers code updates up to 94.7% faster.</li>
</ul>
<p>These gains aren’t just theoretical; they reflect tangible improvements, especially in larger applications with extensive module graphs. With 90% of the Next.js dev tests now successfully passing, you can anticipate consistently faster and more reliable performance when using next dev — turbo.</p>
<p>As we approach the milestone of 100% test passage, Turbopack will transition to a stable state in an upcoming minor release. Rest assured, we remain committed to supporting the use of Webpack for custom configurations and ecosystem plugins.</p>
<h3>Server Actions</h3>
<p>In Next.js 14, Server Actions represent a substantial evolution in granting developers greater authority over server-side rendering. This empowering enhancement equips developers with the capability to fetch essential data from the server before page generation, ensuring that vital information is readily available when the page loads. This not only accelerates the initial load times but also minimizes superfluous client-side requests.</p>
<p>These Server Actions are seamlessly woven into the fabric of the entire App Router model, offering a versatile toolkit that enables developers to:</p>
<ul>
<li>Dynamically revalidate cached data using functions like revalidatePath() or revalidateTag().</li>
<li>Effortlessly steer users to different routes with the redirect() method.</li>
<li>Manage cookies efficiently through the cookies() function, both for setting and retrieval.</li>
<li>Harness optimistic UI updates, enhancing user experience, with the useOptimistic() utility.</li>
<li>Handle and elegantly display server-side errors through the useFormState() function.</li>
<li>Keep users informed with loading states on the client side, thanks to the useFormStatus() function.</li>
</ul>
<p>This multifaceted suite of capabilities empowers developers to not only fine-tune server-side rendering but also to create more responsive and robust web applications with Next.js 14.</p>
<h3>Enhanced Metadata Options</h3>
<p>Importantly, the incorporation of meta tags into the initial page content ensures a seamless user experience. This practice prevents issues like page flickering due to theme color changes or layout shifts resulting from viewport adjustments.</p>
<p>Moreover, Next.js 14 distinguishes between blocking and non-blocking metadata, enabling a smoother experience. Only a select few metadata options are marked as blocking, ensuring that non-blocking metadata won’t hinder partially prerendered pages from serving the static shell.</p>
<p>Notably, the following metadata options are now deprecated and will be phased out in future major releases:</p>
<ul>
<li>viewport: Previously responsible for setting initial zoom and viewport properties.</li>
<li>colorScheme: Used for specifying support modes (light/dark) for the viewport.</li>
<li>themeColor: Defined the colour for the chrome surrounding the viewport.</li>
</ul>
<p>Next.js 14 introduces the replacement options of viewport and generateViewport while retaining all other metadata options.</p>
<h3>Partial Prerendering</h3>
<p>The newest star in the release of Next.js 14 is Partial Prerendering. In a world where there is a constant battle between SSR and SSG, Next.js 14 gives you the best of both worlds. It provides you with a fast initial static response whilst streaming dynamic content based on your React Suspense boundaries, all this whilst eliminating the need to learn any new APIs. Thus giving you the speed of static sites and the dynamism of server-rendered applications.</p>
<p>Conclusion</p>
<p>Next.js 14 represents a significant step forward for the framework. Focusing on improving existing features rather than adding new ones, this version offers developers a sleeker, more efficient, and powerful experience. With Turbopack, Server Actions, Partial Prerendering, and integration with Strapi and artificial intelligence, Next.js 14 stands out as an ideal solution for developing modern and high-performance web applications. Developers now have an even more robust and versatile tool for creating innovative and effective web experiences.</p>
<p>To begin development with Next.js 14, you can make use of the <a href="https://nextjs.org/docs">official documentation</a> or reference the <a href="https://nextjs.org/learn">Next.js 14 learn</a> available on the website.</p>
<p>By Jatin Jain Saraf on February 26, 2024.</p>]]></content:encoded>
      <pubDate>Mon, 26 Feb 2024 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Next.js</category>
      <category>Frontend</category>
      <category>React</category>
    </item>
    <item>
      <title>Linux Terminal Basics Commands:</title>
      <link>https://insight.jatinjainsaraf.com/linux-terminal-basics-commands</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/linux-terminal-basics-commands</guid>
      <description>File and Directory Commands: ls: List files and directories. Syntax: ls [options] [directory] cd: Change directory. Syntax: cd [directory]…</description>
      <content:encoded><![CDATA[<h1>Linux Terminal Basics Commands:</h1>
<p>File and Directory Commands: ls: List files and directories. Syntax: ls [options] [directory] cd: Change directory. Syntax: cd [directory]…</p>
<hr>
<h3>Linux Terminal Basics Commands:</h3>
<blockquote>
<p><strong><em>File and Directory Commands:</em><br>
<em>ls</em></strong>: List files and directories. <strong><em>Syntax</em></strong>: ls [options] [directory]<br>
<strong><em>cd</em></strong>: Change directory. <strong><em>Syntax</em></strong>: cd [directory]<br>
<strong><em>pwd</em></strong>: Print working directory. <strong><em>Syntax</em></strong>: pwd<br>
<strong><em>mkdir</em></strong>: Create a new directory. <strong><em>Syntax</em></strong>: mkdir [directory]<br>
<strong><em>cp</em></strong>: Copy files or directories. <strong><em>Syntax</em></strong>: cp [options] source destination<br>
<strong><em>mv</em></strong>: Move or rename files/directories. <strong><em>Syntax</em></strong>: mv [options] source destination<br>
<strong><em>rm</em></strong>: Remove/delete files or directories. <strong>Syntax</strong>: rm [options] file</p>
</blockquote>
<blockquote>
<p><strong><em>File Manipulation:<br>
cat</em></strong>: Concatenate and display the content of files. <strong><em>Syntax</em></strong>: cat [file]<br>
<strong><em>nano</em></strong> Text editors to create or edit files. <strong><em>Syntax (nano):</em></strong> nano [file]<strong>_<br>
Vim_</strong>: Text editors to create or edit files.<strong><em>Syntax (vim)</em></strong>: vim [file]<br>
<strong><em>touch</em></strong>: Create an empty file or update timestamp. <strong><em>Syntax</em></strong>: touch [file]</p>
</blockquote>
<blockquote>
<p><strong><em>System Information:<br>
uname</em></strong> -a: Display system information. <strong><em>Syntax</em></strong>: uname -a<br>
<strong><em>df -h</em></strong>: Show disk space usage. <strong><em>Syntax</em></strong>: df -h<br>
<strong><em>free -h</em></strong>: Display RAM usage. <strong><em>Syntax</em></strong>: free -h<br>
<strong>t_op or htop_</strong>: Display running processes. <strong><em>Syntax</em></strong>: top or htop</p>
</blockquote>
<blockquote>
<p><strong><em>Debian/Ubuntu:</em></strong><br>
<strong><em>sudo apt update</em></strong>: Update package lists. <strong><em>Syntax</em></strong>: sudo apt update<br>
<strong><em>sudo apt upgrade:</em></strong> Upgrade installed packages. <strong><em>Syntax</em></strong>: sudo apt upgrade<br>
<strong><em>sudo apt install</em></strong> : Install a new package. <strong><em>Syntax</em></strong>: sudo apt install [package]</p>
</blockquote>
<blockquote>
<p><strong><em>Red Hat/Fedora:</em></strong><br>
<strong><em>sudo yum update:</em></strong> Update packages. <strong><em>Syntax</em></strong>: sudo yum update<br>
<strong><em>sudo yum install:</em></strong> Install a new package. <strong><em>Syntax</em></strong>: sudo yum install [package]</p>
</blockquote>
<blockquote>
<p><strong><em>Node.js</em></strong><br>
<strong><em>node -v:</em></strong> Check Node.js version. <strong><em>Syntax</em></strong>: node -v<br>
<strong><em>npm -v:</em></strong> Check npm version. <strong><em>Syntax</em></strong>: npm -v<br>
<strong><em>npm init:</em></strong> Initialize a new Node.js project. <strong><em>Syntax</em></strong>: npm init<br>
<strong><em>node app.js:</em></strong> Run a Node.js script. <strong><em>Syntax</em></strong>: node [script]</p>
</blockquote>
<blockquote>
<p><strong><em>NPM</em></strong>:<br>
<strong><em>npm install :</em></strong> Install a Node.js package.<strong><em>Syntax</em></strong>: npm install [package]<br>
<strong><em>npm install -g :</em></strong> Install a global package.<strong><em>Syntax</em></strong>: npm install -g [package]<br>
<strong><em>npm start</em></strong><em>:</em> Start the application (as defined in package.json).<strong><em>Syntax</em></strong>: npm start<br>
<strong><em>npm test:</em></strong> Run tests.<strong><em>Syntax</em></strong>: npm test<br>
<strong><em>npm run </em></strong><em> Run a custom script.<strong><em>Syntax</em></strong>: npm run [script]</em></p><em>
</em></blockquote><em>
<h3><strong><em>MongoDB Commands:</em></strong></h3>
<blockquote>
<p><strong><em>MongoDB Service:<br>
Sudo service mongod start:</em></strong> Start MongoDB service. <strong><em>Syntax</em></strong>: sudo service mongod start<br>
<strong><em>sudo service mongod stop:</em></strong> Stop MongoDB service.<strong><em>Syntax</em></strong>: sudo service mongod stop<br>
<strong><em>sudo service mongod restart:</em></strong> Restart MongoDB service. <strong><em>Syntax</em></strong>: sudo service mongod restart</p>
</blockquote>
<blockquote>
<p><strong><em>Mongo Shell:<br>
mongo:</em></strong> Open the MongoDB shell. <strong><em>Syntax</em></strong>: mongo<br>
<strong><em>show dbs:</em></strong> Show available databases.<strong><em>Syntax</em></strong>: show dbs<br>
<strong><em>use :</em></strong> Switch to a specific database.<strong><em>Syntax</em></strong>: use [database]<br>
<strong><em>db..find():</em></strong> Retrieve documents from a collection. <strong><em>Syntax</em></strong>: db.[collection].find()</p>
</blockquote>
<h3><strong><em>Git Commands:</em></strong></h3>
<blockquote>
<p><strong><em>Repository Management:<br>
git init:</em></strong> Initialize a new Git repository. <strong><em>Syntax</em></strong>: git init<br>
<strong><em>git clone :</em></strong> Clone a repository. <strong><em>Syntax</em></strong>: git clone [repository]<br>
<strong><em>git add .:</em></strong> Stage all changes for commit.<strong><em>Syntax</em></strong>: git add .<br>
<strong><em>git commit -m “message”:</em></strong> Commit changes with a message. <strong><em>Syntax</em></strong>: git commit -m “message”</p>
</blockquote>
<blockquote>
<p><strong><em>Branching:<br>
git branch:</em></strong> List branches.<strong><em>Syntax</em></strong>: git branch<br>
<strong><em>git checkout :</em></strong> Switch to a different branch.<strong><em>Syntax</em></strong>: git checkout [branch]<br>
<strong><em>git merge :</em></strong> Merge a branch into the current branch.<strong><em>Syntax</em></strong>: git merge [branch]</p>
</blockquote>
<blockquote>
<p><strong><em>Remote Repositories:<br>
git remote add origin :</em></strong> Add a remote repository. <strong><em>Syntax</em></strong>: git remote add origin [repository]<br>
<strong><em>git push -u origin :</em></strong> Push changes to a remote repository.<strong><em>Syntax</em></strong>: git push -u origin [branch]<br>
<strong><em>git pull origin :</em></strong> Pull changes from a remote repository.<strong><em>Syntax</em></strong>: git pull origin [branch]</p>
</blockquote>
<h3><strong><em>Docker Commands:</em></strong></h3>
<blockquote>
<p><strong><em>Container Management:<br>
docker build -t :</em></strong> Build a Docker image. <strong><em>Syntax</em></strong>: docker build -t [image-name]<br>
<strong><em>docker run -p :</em></strong>  <img>: Run a Docker container. <strong><em>Syntax</em></strong>: docker run -p [host-port]:[container-port] [image]<br>
<strong><em>docker ps:</em></strong> List running containers.<strong><em>Syntax</em></strong>: docker ps<br>
<strong><em>docker stop :</em></strong> Stop a running container. <strong><em>Syntax</em></strong>: docker stop [container-id]</p>
</blockquote>
<blockquote>
<p><strong><em>Image and Registry:<br>
docker images:</em></strong> List Docker images.<strong><em>Syntax</em></strong>: docker images<br>
<strong><em>docker rmi :</em></strong> Remove a Docker image.<strong><em>Syntax</em></strong>: docker rmi [image-id]<br>
<strong><em>docker push <img>:</em></strong> Push an image to a Docker registry. <strong><em>Syntax</em></strong>: docker push [image]</p>
</blockquote>
<p>By Jatin Jain Saraf on December 20, 2023.</p></em>]]></content:encoded>
      <pubDate>Wed, 20 Dec 2023 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Linux</category>
      <category>DevOps</category>
    </item>
    <item>
      <title>JavaScript ES6 Coding Conventions</title>
      <link>https://insight.jatinjainsaraf.com/javascript-es6-coding-conventions</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/javascript-es6-coding-conventions</guid>
      <description>ES Conventions ES6</description>
      <content:encoded><![CDATA[<h1>ES Conventions</h1>
<p>ES6</p>
<hr>
<h3>ES Conventions</h3>
<p><strong>ES6</strong></p>
<ul>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_let">The let keyword</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_const">The const keyword</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_arrow">Arrow Functions</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_spread">The … Operator</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_forof">For/of</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_map">Map Objects</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_set">Set Objects</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_class">Classes</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_promise">Promises</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_symbol">Symbol</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_param">Default Parameters</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_rest">Function Rest Parameter</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_includes">String.includes()</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_startswith">String.startsWith()</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_endswith">String.endsWith()</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_array_from">Array.from()</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_array_keys">Array keys()</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_array_find">Array find()</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_array_findIndex">Array findIndex()</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_math_methods">New Math Methods</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_number_properties">New Number Properties</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_number_methods">New Number Methods</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_global_methods">New Global Methods</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_entries">Object entries</a></li>
<li><a href="https://www.w3schools.com/js/js_es6.asp#mark_modules">JavaScript Modules</a></li>
</ul>
<p><strong>ES7</strong></p>
<ul>
<li>JavaScript Exponentiation (**)</li>
<li>JavaScript Exponentiation assignment (**=)</li>
<li>JavaScript Array includes()</li>
</ul>
<p><strong>ES8</strong></p>
<ul>
<li><a href="https://www.w3schools.com/js/js_2017.asp#mark_padding">JavaScript String padding</a></li>
<li><a href="https://www.w3schools.com/js/js_2017.asp#mark_obj_entries">JavaScript Object entries()</a></li>
<li><a href="https://www.w3schools.com/js/js_2017.asp#mark_obj_values">JavaScript Object values()</a></li>
<li><a href="https://www.w3schools.com/js/js_2017.asp#mark_async">JavaScript async and await</a></li>
<li>JavaScript Object.getOwnPropertyDescriptors</li>
</ul>
<p><strong>ES9</strong></p>
<ul>
<li><a href="https://www.w3schools.com/js/js_2018.asp#mark_async_iteration">Asynchronous Iteration</a></li>
<li><a href="https://www.w3schools.com/js/js_2018.asp#mark_promise_finally">Promise Finally</a></li>
<li><a href="https://www.w3schools.com/js/js_2018.asp#mark_obj_rest">Object Rest Properties</a></li>
<li><a href="https://www.w3schools.com/js/js_2018.asp#mark_regxp">New RegExp Features</a></li>
<li><a href="https://www.w3schools.com/js/js_2018.asp#mark_regxp">JavaScript Shared Memory</a></li>
</ul>
<p><strong>ES10</strong></p>
<ul>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_trim_start">String.trimStart()</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_trim_end">String.trimEnd()</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_from_entries">Object.fromEntries</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_omit_catch">Optional catch binding</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_array_flat">Array.flat()</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_array_flatmap">Array.flatMap()</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_array_sort">Revised Array.Sort()</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_json_stringify">Revised JSON.stringify()</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_separator_symbols">Separator symbols allowed in string litterals</a></li>
<li><a href="https://www.w3schools.com/js/js_2019.asp#mark_function_tostring">Revised Function.toString()</a></li>
</ul>
<p><strong>ES11</strong></p>
<ul>
<li><a href="https://www.w3schools.com/js/js_2020.asp#mark_bigint">BigInt</a></li>
<li><a href="https://www.w3schools.com/js/js_2020.asp#mark_string_matchall">String matchAll()</a></li>
<li><a href="https://www.w3schools.com/js/js_2020.asp#mark_nullish_coalescing">The Nullish Coalescing Operator (??)</a></li>
<li><a href="https://www.w3schools.com/js/js_2020.asp#mark_optional_chaining">The Optional Chaining Operator (?.)</a></li>
<li><a href="https://www.w3schools.com/js/js_2020.asp#mark_assign_logical_and">Logical AND Assignment Operator (&#x26;&#x26;=)</a></li>
<li><a href="https://www.w3schools.com/js/js_2020.asp#mark_assign_logical_or">Logical OR Assignment (||=)</a></li>
<li><a href="https://www.w3schools.com/js/js_2020.asp#mark_assign_nullish">Nullish Coalescing Assignment (??=)</a></li>
<li>Promise allSettled():<br>
Promise.allSettled([prom1,prom2,prom3]).then {}</li>
<li>Dynamic Import</li>
</ul>
<p><strong>ES12</strong></p>
<ul>
<li>Promise any()<br>
const first = await Promise.any([prom1,prom2,prom3]);</li>
<li><a href="https://www.w3schools.com/js/js_2021.asp#mark_string_replaceall">String replaceAll()</a></li>
<li><a href="https://www.w3schools.com/js/js_2021.asp#mark_numeric_separators">Numeric Separators (_)</a></li>
</ul>
<p>By Jatin Jain Saraf on December 19, 2023.</p>]]></content:encoded>
      <pubDate>Tue, 19 Dec 2023 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Engineering</category>
    </item>
    <item>
      <title>40 Best Practices for Writing Clean JavaScript, React, and Node.js Code</title>
      <link>https://insight.jatinjainsaraf.com/40-best-practices-for-writing-clean-javascript-react-and-nodejs-code</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/40-best-practices-for-writing-clean-javascript-react-and-nodejs-code</guid>
      <description>When working on JavaScript, React, or Node.js projects, following best practices is essential for code readability, maintaina</description>
      <content:encoded><![CDATA[<h1>40 Best Practices for Writing Clean JavaScript, React, and Node.js Code</h1>
<p>When working on JavaScript, React, or Node.js projects, following best practices is essential for code readability, maintainability, and…</p>
<hr>
<h3>40 Best Practices for Writing Clean JavaScript, React, and Node.js Code</h3>
<p>When working on JavaScript, React, or Node.js projects, following best practices is essential for code readability, maintainability, and overall code quality. In this article, we’ll explore 40 best practices to help you write clean and efficient code with these technologies.</p>
<ol>
<li><strong>Import Order Should be Correct:</strong> Organize imports logically, typically grouping third-party libraries, local modules, and built-in modules. This enhances code readability and maintainability. <a href="https://eslint.org/docs/rules/sort-imports"><strong>Source</strong></a></li>
<li><strong>Constant Values and Strings Should be in a Separate File:</strong> Centralize constants, strings, and static values in a separate file for better management and code reusability. <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"><strong>Source</strong></a></li>
<li><strong>2 Space Indentation:</strong> Maintain consistent indentation (typically two spaces) to improve code consistency and readability. <a href="https://standardjs.com/rules.html#indent"><strong>Source</strong></a></li>
<li><strong>Capitalized Validation Messages:</strong> Format validation messages with proper capitalization for a professional look and feel. <a href="https://www.toptal.com/software/10-common-reactjs-mistakes-and-how-to-avoid-them"><strong>Source</strong></a></li>
<li><strong>Use Hooks in Every Case:</strong> Utilize React hooks for managing state, effects, and other React-related functionalities. They provide a more concise and functional approach. <a href="https://reactjs.org/docs/hooks-intro.html"><strong>Source</strong></a></li>
<li><strong>Convert Class Components to Functional Components:</strong> Refactor class-based components to functional components using hooks, as they are the modern and recommended approach. <a href="https://reactjs.org/docs/hooks-intro.html"><strong>Source</strong></a></li>
<li><strong>Convert Normal Function to ES6 Arrow Function:</strong> Refactor traditional functions to ES6 arrow functions for a more concise syntax and lexical scoping benefits. <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions"><strong>Source</strong></a></li>
<li><strong>Avoid Using Lodash `__get` for Static Data Access:</strong> When accessing static data, avoid using special libraries like Lodash for simplicity and readability. Direct access is preferred. <a href="https://lodash.com/docs/4.17.15#get"><strong>Source</strong></a></li>
<li><strong>Use Nullish Coalescing and Optional Chaining:</strong> Utilize nullish coalescing (`??`) and optional chaining (`?.`) operators for safer and more concise code, especially when dealing with potentially null or undefined values. <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator"><strong>Source</strong></a></li>
<li><strong>Destructure Repeatedly Used Properties:</strong> Destructure object properties when they’re used multiple times within a function or component to make the code cleaner and more readable. <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment"><strong>Source</strong></a></li>
<li><strong>Remove Unused/Redundant Props:</strong> Eliminate unused or redundant props from components to streamline code and reduce clutter. <a href="https://reactjs.org/docs/composition-vs-inheritance.html#props-children"><strong>Source</strong></a></li>
<li><strong>Check Console for Warnings After Refactoring:</strong> Ensure that the console doesn’t show any warning messages after refactoring code to maintain code quality and stability.</li>
<li><strong>Review Data from API Calls:</strong> Examine API responses in the network tab to identify and remove unnecessary data not being used in the application for improved performance.</li>
<li><strong>Camel Case Naming Convention:</strong> Follow the camel case naming convention for variables, functions, and other identifiers to maintain code consistency and readability. <a href="https://javascript.info/naming-conventions"><strong>Source</strong></a></li>
<li><strong>Avoid Function Names Starting With Underscore:</strong> Refrain from using underscores as the starting character in function names, as it may imply privacy in some contexts not applicable in JavaScript. Use meaningful names instead. <a href="https://stackoverflow.com/questions/5525089/underscore-prefix-for-property-and-method-names-in-javascript"><strong>Source</strong></a></li>
<li><strong>Remove Unnecessary Div/Fragments:</strong> Eliminate unnecessary `<div>` or `` elements to simplify the component structure and improve code readability. <a href="https://reactjs.org/docs/fragments.html"><strong>Source</strong></a></div></li>
<li><strong>Use `console.error` and `console.info`:</strong> Utilize `<strong>console.error</strong>` for displaying error messages and `<strong>console.info</strong>` for informative messages in the console, aiding in debugging and monitoring. <a href="https://developer.mozilla.org/en-US/docs/Web/API/Console/error"><strong>Source</strong></a></li>
<li><strong>Proper Try-Catch Blocks:</strong> Implement appropriate try-catch blocks for error handling, especially when dealing with potentially failing operations. <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch"><strong>Source</strong></a></li>
<li><strong>Avoid One-Line Functions for State Changes:</strong> Refrain from using one-liners for complex state changes or side effects to maintain code readability and clarity.</li>
<li><strong>Extract Reusable Components</strong>: Identify patterns in your code and extract common logic into reusable components or custom hooks to reduce duplication and follow the DRY (Don’t Repeat Yourself) principle. <a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"><strong>Source</strong></a></li>
<li><strong>Avoid Magic Numbers:</strong> Replace numeric literals with named constants or variables that describe their purpose for better code comprehension. <a href="https://refactoring.guru/replace-magic-number-with-symbolic-constant"><strong>Source</strong></a></li>
<li><strong>Limit Function/Component Length:</strong> Break down large functions or components into smaller, focused units to improve readability, maintainability, and testability. <a href="https://en.wikipedia.org/wiki/Single-responsibility_principle"><strong>Source</strong></a></li>
<li><strong>Use Descriptive Variable Names:</strong> Choose descriptive variable and function names that convey their purpose and usage, reducing the need for excessive comments. <a href="https://javascript.info/naming-conventions"><strong>Source</strong></a></li>
<li><strong>Consistent Naming Conventions:</strong> Maintain consistent naming conventions across the codebase for better understanding and collaboration. <a href="https://eslint.org/docs/rules/id-length"><strong>Source</strong></a></li>
<li><strong>Optimize Render Performance:</strong> Use memoization techniques like `React.memo`, `useMemo`, and `useCallback` to optimize rendering performance of components by avoiding unnecessary re-renders. <a href="https://reactjs.org/docs/react-api.html#reactmemo"><strong>Source</strong></a></li>
<li><strong>Separation of Concerns:</strong> Ensure that each component or function has a clear and singular responsibility, adhering to the principle of separation of concerns, which improves code modularity and maintainability. <a href="https://en.wikipedia.org/wiki/Separation_of_concerns"><strong>Source</strong></a></li>
<li><strong>Use CSS Modules or Styled Components:</strong> Implement CSS Modules or a CSS-in-JS solution like Styled Components to encapsulate styling within components, preventing global style conflicts and enhancing maintainability. <a href="https://styled-components.com/docs"><strong>Source</strong></a></li>
<li><strong>Responsive Design</strong>: Ensure your components and UI are responsive across different screen sizes and devices for an enhanced user experience. <a href="https://web.dev/articles/responsive-web-design-basics"><strong>Source</strong></a></li>
<li><strong>Document Code:</strong> Include comments, documentation, and explanations for complex logic, algorithms, or non-obvious decisions to aid in understanding and maintaining the codebase. <a href="https://en.wikipedia.org/wiki/Code_documentation"><strong>Source</strong></a></li>
<li><strong>Use PropTypes or TypeScript:</strong> Enforce type checking and validation using PropTypes (for JavaScript) or TypeScript (for TypeScript projects) to catch type-related errors early and improve code reliability. <a href="https://reactjs.org/docs/typechecking-with-proptypes.html"><strong>Source</strong></a></li>
<li><strong>Avoid Deep Nesting:</strong> Refrain from excessive component nesting, which can make code harder to read and maintain. Find a balance between reusability and nesting depth. <a href="https://reactjs.org/docs/composition-vs-inheritance.html"><strong>Source</strong></a></li>
<li><strong>Consolidate Imports:</strong> Keep import statements organized by consolidating multiple imports from the same module into a single statement, enhancing code readability. <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import"><strong>Source</strong></a></li>
<li><strong>Use CSS Flexbox or Grid:</strong> Utilize modern CSS layout techniques like Flexbox and CSS Grid for building responsive and flexible layouts, improving UI design. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout"><strong>Source</strong></a></li>
<li><strong>Avoid Inline Styles:</strong> Separate styling from components by using external stylesheets or styling solutions like CSS Modules for better code maintainability and separation of concerns. <a href="https://css-tricks.com/the-differing-perspectives-on-css-in-js/"><strong>Source</strong></a></li>
<li><strong>Unit Testing:</strong> Write unit tests using tools like Jest and testing libraries to ensure the correctness of your codebase, prevent regressions, and support continuous integration. <a href="https://jestjs.io/docs/getting-started"><strong>Source</strong></a></li>
<li><strong>Code Reviews:</strong> Regularly conduct code reviews with team members to ensure adherence to coding standards, best practices, and to catch potential issues early in the development process. <a href="https://github.com/thoughtbot/guides/tree/master/code-review"><strong>Source</strong></a></li>
<li><strong>Keep Dependencies Updated:</strong> Regularly update third-party dependencies to benefit from bug fixes, performance improvements, and new features while ensuring security and stability. <a href="https://snyk.io/advisor/npm-package/react"><strong>Source</strong></a></li>
<li><strong>Performance Profiling:</strong> Use tools like React DevTools to profile and optimize the performance of your application, ensuring a smooth user experience. <a href="https://reactjs.org/docs/optimizing-performance.html"><strong>Source</strong></a></li>
<li><strong>Error Boundary Components:</strong> Implement error boundaries to gracefully handle errors and prevent crashes from propagating through the entire application, improving overall stability. <a href="https://reactjs.org/docs/error-boundaries.html"><strong>Source</strong></a></li>
<li><strong>Version Control:</strong> Use version control systems like Git effectively to track changes, collaborate with team members, and maintain a history of your codebase, ensuring code integrity. <a href="https://git-scm.com/doc"><strong>Source</strong></a></li>
</ol>
<p>By following these best practices, you can significantly improve the quality of your JavaScript, React, and Node.js code. It will make your codebase more maintainable, scalable, and easier to collaborate on with other developers. Happy coding!</p>
<p>By Jatin Jain Saraf on September 1, 2023.</p>]]></content:encoded>
      <pubDate>Fri, 01 Sep 2023 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>React</category>
      <category>Frontend</category>
      <category>JavaScript</category>
    </item>
    <item>
      <title>Branching Guidelines and Conventions</title>
      <link>https://insight.jatinjainsaraf.com/branching-guidelines-and-conventions</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/branching-guidelines-and-conventions</guid>
      <description>There are many excellent naming conventions regarding git branches and commits. But what if you want something very lean and simple?</description>
      <content:encoded><![CDATA[<h1>Branching Guidelines and Conventions</h1>
<p>There are many excellent naming conventions regarding git branches and commits. But what if you want something very lean and simple?</p>
<hr>
<h3>Branching Guidelines and Conventions</h3>
<p>There are many excellent naming conventions regarding git branches and commits. But what if you want something very lean and simple?</p>
<p>Here is a proposition.</p>
<h3><strong>Categories of branches</strong></h3>
<p>Every code space should have a main and a development branch.<br>
Apart from them, you can create any number of branches in your code base.</p>
<ul>
<li><strong>main branch:</strong> Changes merged into the main branch will be reflected in a <strong>production environment.</strong></li>
<li><strong>develop branch:</strong> Changes merged into the develop branch will be reflected in a <strong>test environment.</strong> All new feature branches of current sprint will be created from the develop branch. A git branch should start with a category. Pick one of these: <code>feature</code>, <code>bugfix</code>, <code>hotfix, test, nojira</code>.</li>
<li><strong>feature</strong> is for adding, refactoring, or removing a feature that is mentioned on any ticket or story created on Jira/ Youtrack. The feature branch should contain code related to only a single feature or ticket, which will be eventually merged into the develop branch. It should be created like <strong>feature/.</strong></li>
<li><strong>bugfix</strong> is for fixing a bug mentioned on Jira/ Youtrack. These are the fixes to deploy on the test and stage environment. It should be created like <strong>bugfix/.</strong></li>
<li><strong>hotfix</strong> is for changing code with a temporary solution and/or without following the usual process (usually because of an emergency). It is also used to deploy hotfix on production. It should be created like <strong>hotfix/</strong> or <strong>hotfix/.</strong></li>
<li><strong>test</strong> is for experimenting outside of an issue/ticket or having a spike ticket on Jira/ Youtrack. It should be created like <strong>test/.</strong></li>
<li><strong>release</strong> is for the stage environment Changes merged into the release branch will be reflected in the <strong>stage environment.</strong> It should be created like <strong>release/</strong></li>
<li>Nojira is for the work that is not listed on Jira/Youtrack or work that has no reference ticket. It should be created like <strong>nojira/</strong></li>
</ul>
<h3><strong>Git Process</strong></h3>
<h4><strong>During Sprint</strong></h4>
<ol>
<li>Create a feature branch with ticket no. from the develop branch.</li>
<li>Work on that feature branch and at the end of the day, push your changes into that branch with the proper commit message below format: <br>
<strong>feature/ | &#x3C;user id / username> | proper commit description<br>
feature | SU-12 | Updated Error occurring in getting user API</strong></li>
<li>Once your work is done, <strong>rebase</strong> your feature branch with the develop branch and create a Pull request against the <strong>develop</strong> branch, The PR title should be as below:<br>
<strong>feature/ | SU-12|  <br>
feature/ | SU-12| Generate random order number in admin</strong></li>
<li>If the reviewer has posted improvements/fixes in PR, then push commits with the same message format with the proper description</li>
<li>Once PR is ready to merge the reviewer should <strong>squash and merge the branch into develop and delete the feature branch</strong></li>
<li>If QA reports bugs in a test environment, create a new branch from the develop branch with format (<strong>bugfix/</strong>) and generate PR against the develop branch. Reviewer should <strong>squash and merge the branch into develop and delete the fix branch</strong></li>
</ol>
<p><strong>Code freeze (stage release/regression phase)</strong></p>
<ol>
<li>Create a new release branch from the main branch in this format <strong>(release_/&#x3C;_tag> release/1.0.0, release/1.0.1)</strong> If all changes from the develop branch are required for the next release, rebase the develop branch on the newly created release branch by running the below command:<br>
 <strong>Git checkout release/1.0.0<br>
Git rebase develop<br>
Git push origin release/1.0.0 (this will trigger stage deployment)</strong></li>
<li>If only a few features(partial changes) are required for release, then cherry-pick commits from the develop branch into the release branch and push new commits.</li>
<li>Any changes pushed to the release branch will trigger stage deployment.</li>
<li>If any issues/bugs are found during code freeze, the fix needs to deploy on the stage environment first and then on a test environment.</li>
<li>For new bugs, create a new release branch from the last release branch, for example, release/1.0.1 from release/1.0.0, then create a new fix branch out of the new release branch with the below format: <strong>bugfix/</strong></li>
<li>Generate fix branch PR against new release branch(release/1.0.1). The reviewer should <strong>squash and merge the branch into the release branch and delete the fix branch</strong></li>
<li>Once QA gives confirmation that the bug is fixed on the stage environment, the Developer should then rebase the release branch on the develop branch, so that the test environment will also have a new fix</li>
</ol>
<p><strong>Production release:</strong></p>
<ol>
<li>If all features/changes from the stage(release branch) environment are required to be released, then rebase the last release branch(e.g. release/1.0.6) on the main branch.<br>
 Git checkout main<br>
Git rebase release/1.0.6<br>
Git push origin main</li>
<li>Once the changes are pushed, manually trigger production deployment from GitHub actions by selecting the main branch</li>
</ol>
<p><strong>Hotfix</strong></p>
<ol>
<li>Create a new release branch(release/1.2.0) from the master and create a hotfix branch out of the new release branch with the below format: <strong>hotfix/</strong></li>
<li>Work on hotfix and generate PR of hotfix branch against new release branch.</li>
<li>Reviewer should <strong>squash and merge the branch into the release branch and delete the hotfix branch</strong></li>
<li>Once QA gives the confirmation of the hotfix on the stage environment, rebase the release branch on the main branch and trigger manual deployment from GitHub actions in order to deploy the hotfix on production.</li>
<li>Developer should then rebase the release branch on the develop branch so that the hotfix will be deployed on the test environment as well.</li>
</ol>
<p>By Jatin Jain Saraf on August 24, 2023.</p>]]></content:encoded>
      <pubDate>Thu, 24 Aug 2023 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Engineering</category>
    </item>
    <item>
      <title>Sequelize using Postgres dialect</title>
      <link>https://insight.jatinjainsaraf.com/sequelize-using-postgres-dialect</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/sequelize-using-postgres-dialect</guid>
      <description>Introduction to Sequelize</description>
      <content:encoded><![CDATA[<h1>Sequelize using Postgres dialect</h1>
<p>Introduction to Sequelize</p>
<hr>
<h3>Sequelize using Postgres dialect</h3>
<hr>
<h3><strong>Introduction to Sequelize</strong></h3>
<blockquote>
<p><a href="https://sequelize.org/"><strong>Sequelize</strong></a> <em>is a popular Object-Relational Mapping (ORM) library for Node.js, which allows you to interact with relational databases like PostgreSQL using JavaScript/Node.js. It provides an easy way to manage your database operations and simplifies the interaction with the database, abstracting SQL queries into JavaScript code. Some key features of Sequelize are listed below:-</em></p>
</blockquote>
<blockquote>
<p><strong><em>Database Support:</em></strong> <em>Sequelize supports various relational databases, including PostgreSQL, MySQL, SQLite, and MSSQL. This flexibility allows you to switch between different database systems easily.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Models and Associations:</em></strong> <em>Sequelize enables you to define models that represent database tables and their relationships (associations) with other tables. These models provide a structured way to interact with the database and handle complex data relationships.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Data Validation:</em></strong> <em>Sequelize offers built-in validation support for your models, allowing you to define validation rules for each attribute to ensure data integrity and consistency.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Migrations</em></strong><em>: With Sequelize, you can use migrations to manage changes to the database schema over time. Migrations help keep your database schema in sync with your application’s codebase and facilitate database version control.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Querying</em></strong><em>: Sequelize provides a straightforward API for querying the database, including support for complex queries like filtering, ordering, aggregating, and pagination.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Hooks</em></strong><em>: Sequelize allows you to define hooks that get executed before or after certain events, such as before saving a record or after fetching data. This feature is helpful for performing additional actions or validation during these events.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Transactions</em></strong><em>: Sequelize supports transactions, which are crucial for maintaining data consistency and integrity when dealing with multiple related operations that must either all succeed or all fail.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Raw SQL Queries:</em></strong> <em>While Sequelize provides a high-level API for interacting with the database, it also allows you to execute raw SQL queries directly when more complex operations are required.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Eager Loading:</em></strong> <em>Sequelize supports eager loading, enabling you to fetch related data along with the main query, which can help avoid the N+1 problem and improve query performance.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Pluggable Dialects:</em></strong> <em>The library is built to support multiple SQL dialects, making it adaptable to various relational database systems.</em></p>
</blockquote>
<hr>
<h3>Introduction to <em>PostgreSQL</em></h3>
<blockquote>
<p><a href="https://www.postgresql.org/"><strong>Postgres</strong></a><em>, also known as PostgreSQL, is a powerful open-source relational database management system (RDBMS). It was initially developed at the University of California, Berkeley in the 1980s and has since grown into a widely used and respected database system. It is a versatile and reliable database system that is widely used by developers and organizations worldwide for various applications ranging from simple web applications to complex enterprise solutions. It has a vibrant community and is continually being improved with each release. Some key features of Sequelize are listed below:-</em></p>
</blockquote>
<blockquote>
<p><strong><em>Relational Database Management System:</em></strong> <em>PostgreSQL is based on the relational model, allowing you to define tables with rows and columns to store data.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Open Source:</em></strong> <em>It is released under the PostgreSQL License, which is a permissive open-source license.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Cross-Platform:</em></strong> <em>PostgreSQL runs on various platforms, including Windows, macOS, Linux, and other UNIX-like operating systems.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Extensible</em></strong><em>: PostgreSQL supports user-defined data types, functions, and procedures, which allows you to create custom extensions to meet specific needs.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Highly Scalable:</em></strong> <em>It can handle large amounts of data and concurrent users, making it suitable for both small projects and large-scale enterprise applications.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Advanced Data Types:</em></strong> <em>PostgreSQL supports a wide range of data types, including numeric types, string types, date/time types, JSON, arrays, and more.</em></p>
</blockquote>
<blockquote>
<p><strong><em>ACID Compliant:</em></strong> <em>PostgreSQL follows the principles of ACID (Atomicity, Consistency, Isolation, Durability) to ensure data integrity and reliability.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Full Text Search:</em></strong> <em>PostgreSQL provides advanced full-text search capabilities, making it suitable for applications that require efficient text search functionality.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Foreign Data Wrappers (FDW):</em></strong> <em>PostgreSQL supports FDWs, which allow you to access external data sources like other databases or APIs seamlessly.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Highly Customizable:</em></strong> <em>PostgreSQL allows fine-grained configuration settings, enabling you to optimize its behavior for specific workloads.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Triggers and Stored Procedures:</em></strong> <em>PostgreSQL supports triggers and stored procedures, enabling you to define complex business logic directly within the database.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Replication</em></strong><em>: PostgreSQL offers various replication options to ensure high availability and fault tolerance.</em></p>
</blockquote>
<hr>
<h3>Postgres Installation</h3>
<blockquote>
<p><strong><em>To install PostgreSQL on Ubuntu 20.04 (or any other Ubuntu version), follow these steps:</em></strong></p>
</blockquote>
<blockquote>
<p><em>Install PostgreSQL and its required components:</em></p>
</blockquote>
<p>sudo apt install postgresql postgresql-contrib</p>
<blockquote>
<p><em>Start the PostgreSQL service:</em></p>
</blockquote>
<p>sudo systemctl start postgresql</p>
<blockquote>
<p><em>You can now connect to PostgreSQL using the `psql` command-line tool. Switch to the `postgres` user and then run `psql`:</em></p>
</blockquote>
<p>bash<br>
sudo -u postgres psql</p>
<blockquote>
<p><strong><em>Create a New User Role:</em></strong> <em>Replace</em> <code>_newusername_</code> <em>with the desired username for the new role.</em></p>
</blockquote>
<p>CREATE USER newusername;</p>
<blockquote>
<p><strong><em>Set Password (Optional):</em></strong> <em>You can set a password for the new user role. Replace</em> <code>_newusername_</code> <em>and</em> <code>_newpassword_</code> <em>with the actual username and password</em></p>
</blockquote>
<p>ALTER USER &#x3C;newusername> WITH PASSWORD &#x3C;newpassword>;</p>
<blockquote>
<p><strong><em>Grant Permissions (Optional):</em></strong> <em>You can grant specific permissions to the new user role. For example, to grant the user role the ability to create databases and roles</em></p>
</blockquote>
<p>ALTER USER newusername CREATEDB;<br>
ALTER USER newusername CREATEROLE;</p>
<hr>
<h3>Sequelize Installation</h3>
<blockquote>
<p><strong><em>To install follow these setps</em></strong></p>
</blockquote>
<p>npm install sequelize sequelize-cli pg</p>
<blockquote>
<p><code>_sequelize_</code><em>: The Sequelize library itself.</em></p>
</blockquote>
<blockquote>
<p><code>_sequelize-cli_</code><em>: The Sequelize Command Line Interface (CLI) for managing database migrations, models, etc.</em></p>
</blockquote>
<blockquote>
<p><code>_pg_</code><em>: The PostgreSQL database driver.</em></p>
</blockquote>
<blockquote>
<p><em>After installing Sequelize, you’ll need to set up the Sequelize configuration and models for your project. Run the following command to initialize Sequelize in your project:</em></p>
</blockquote>
<p>npx sequelize-cli init</p>
<blockquote>
<p><em>This will create the necessary directories and files for Sequelize in your project.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Configure Database:</em></strong> <em>Open the</em> <code>_config/config.json_</code> <em>file that was generated by Sequelize initialization and configure your database connection settings. You'll need to provide the database name, username, password, host, and dialect (e.g., "postgres" for PostgreSQL).</em></p>
</blockquote>
<blockquote>
<p><strong><em>Create Models:</em></strong> <em>Models represent your database tables and are defined using Sequelize. Create a new model using the Sequelize CLI</em></p>
</blockquote>
<p>npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,email:string</p>
<blockquote>
<p><strong><em>Migrations and Database Setup:</em></strong> <em>Sequelize uses migrations to manage changes to the database schema. To create the initial migration and apply it to the database, run</em></p>
</blockquote>
<p>npx sequelize-cli db:migrate</p>
<blockquote>
<p><em>This will create the necessary tables in your database based on your model definitions.</em></p>
</blockquote>
<hr>
<h3><strong>Establish a connection to Postgres using sequelize</strong></h3>
<blockquote>
<p><strong>Configure Sequelize Connection:</strong></p>
</blockquote>
<p>const { Sequelize, DataTypes } = require('sequelize');</p>
<p>// Configure database connection<br>
const sequelize = new Sequelize('database', 'username', 'password', {<br>
host: 'localhost',<br>
dialect: 'postgres',<br>
});</p>
<p>// Define User model<br>
const User = sequelize.define('User', {<br>
username: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
email: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
});</p>
<p>// Define Post model<br>
const Post = sequelize.define('Post', {<br>
title: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
content: {<br>
type: DataTypes.TEXT,<br>
allowNull: false,<br>
},<br>
});</p>
<p>// Set up associations<br>
User.hasMany(Post);<br>
Post.belongsTo(User);</p>
<p>// Sync models with the database<br>
sequelize.sync({ force: true }) // WARNING: This will drop existing tables and recreate them<br>
.then(() => {<br>
console.log('Models synchronized with database.');<br>
})<br>
.catch((error) => {<br>
console.error('Error synchronizing models:', error);<br>
});</p>
<hr>
<h3><strong>Model Usages</strong></h3>
<blockquote>
<p><em>In Sequelize, models serve as JavaScript representations of database tables. They define the structure of the data that will be stored in the database and provide an interface for querying, creating, updating, and deleting records. Let’s explore how to use models in Sequelize:</em></p>
</blockquote>
<blockquote>
<p><strong><em>Defining a Model:</em></strong> <em>To define a model, you use the</em> <code>_sequelize.define()_</code> <em>method. Here's an example of defining a</em> <code>_User_</code> <em>model:</em></p>
</blockquote>
<p>const { Sequelize, DataTypes } = require('sequelize');</p>
<p>const sequelize = new Sequelize('database', 'username', 'password', {<br>
host: 'localhost',<br>
dialect: 'postgres',<br>
});</p>
<p>const User = sequelize.define('User', {<br>
username: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
email: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
});</p>
<blockquote>
<p><strong><em>Creating Records:</em></strong> <em>You can create records using the</em> <code>_create()_</code> <em>method on the model:</em></p>
</blockquote>
<p>User.create({<br>
username: 'john_doe',<br>
email: '<a href="mailto:john@example.com">john@example.com</a>',<br>
})<br>
.then((user) => {<br>
console.log('New user created:', user.toJSON());<br>
})<br>
.catch((error) => {<br>
console.error('Error creating user:', error);<br>
});</p>
<blockquote>
<p><strong><em>Querying Records:</em></strong> <em>You can query records using methods like</em> <code>_findOne()_</code><em>,</em> <code>_findAll()_</code><em>, and custom query methods. For example:</em></p>
</blockquote>
<p>User.findOne({<br>
where: { username: 'john_doe' }<br>
})<br>
.then((user) => {<br>
if (user) {<br>
console.log('Found user:', user.toJSON());<br>
} else {<br>
console.log('User not found.');<br>
}<br>
})<br>
.catch((error) => {<br>
console.error('Error querying user:', error);<br>
});</p>
<blockquote>
<p><strong><em>Updating Records:</em></strong> <em>You can update records using the</em> <code>_update()_</code> <em>method:</em></p>
</blockquote>
<p>User.update({ username: 'new_username' }, {<br>
where: { id: 1 }<br>
})<br>
.then((result) => {<br>
console.log('Updated user:', result);<br>
})<br>
.catch((error) => {<br>
console.error('Error updating user:', error);<br>
});</p>
<blockquote>
<p><strong><em>Deleting Records:</em></strong> <em>You can delete records using the</em> <code>_destroy()_</code> <em>method:</em></p>
</blockquote>
<p>User.destroy({<br>
where: { id: 1 }<br>
})<br>
.then(() => {<br>
console.log('User deleted.');<br>
})<br>
.catch((error) => {<br>
console.error('Error deleting user:', error);<br>
});</p>
<hr>
<h3><strong>DataTypes</strong></h3>
<blockquote>
<p><em>In the above snippet, we have used DataTypes &#x26; Sequelize, Sequelize, and DataTypes are key components of the Sequelize library, which is an Object-Relational Mapping (ORM) tool for Node.js. Sequelize simplifies database interactions by allowing you to work with databases using JavaScript objects and methods, rather than writing raw SQL queries. Here’s a brief introduction to both Sequelize and DataTypes:</em></p>
</blockquote>
<blockquote>
<p><em>1.)</em> <strong><em>Sequelize</em></strong> <em>is a popular ORM library that provides an abstraction layer for working with relational databases, making it easier to manage and manipulate database records through JavaScript code. It supports various relational database systems, including PostgreSQL, MySQL, SQLite, and MSSQL.</em><strong><em>Key features of Sequelize include: <br>
Model Definition:</em></strong> <em>Sequelize allows you to define models, which are JavaScript classes that represent tables in your database. Models define the structure of the table, including columns, data types, and associations with other tables.</em><strong><em>Migrations:</em></strong> <em>Sequelize provides a way to manage database schema changes over time using migrations. Migrations are scripts that define changes to the database schema, such as adding or modifying tables and columns.</em><strong><em>Querying:</em></strong> <em>Sequelize offers a query interface for creating, reading, updating, and deleting records from the database. It supports both simple and complex queries.</em><strong><em>Associations:</em></strong> <em>Sequelize allows you to define relationships (associations) between different models, such as one-to-one, one-to-many, and many-to-many relationships.</em><strong><em>Hooks:</em></strong> <em>Sequelize provides hooks (similar to event listeners) that allow you to perform actions before or after certain database operations, such as creating or updating records.</em><strong><em>Validation:</em></strong> <em>You can define validation rules for model attributes to ensure that data meets specific requirements before being stored in the database.</em></p>
</blockquote>
<blockquote>
<p><em>2.)</em> <strong><em>DataTypes</em></strong> <em>is an object provided by Sequelize that represents the different data types you can use when defining model attributes. It maps JavaScript data types to the corresponding data types in your database.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Common DataTypes include:</em></strong></p>
</blockquote>
<blockquote>
<p><code>_STRING_</code><em>: Represents variable-length character strings.</em></p>
</blockquote>
<blockquote>
<p><code>_INTEGER_</code><em>: Represents whole numbers.</em></p>
</blockquote>
<blockquote>
<p><code>_BOOLEAN_</code><em>: Represents true or false values.</em></p>
</blockquote>
<blockquote>
<p><code>_DATE_</code><em>: Represents date and time values.</em></p>
</blockquote>
<blockquote>
<p><code>_FLOAT_</code><em>,</em> <code>_DOUBLE_</code><em>: Represent floating-point numbers.</em></p>
</blockquote>
<blockquote>
<p><code>_TEXT_</code><em>: Represents large blocks of text.</em></p>
</blockquote>
<blockquote>
<p><code>_ENUM_</code><em>: Represents a fixed set of values.</em></p>
</blockquote>
<blockquote>
<p><em>When defining a model, you use DataTypes to specify the type of each attribute in the model. Here’s a basic example of using Sequelize and DataTypes to define a</em> <code>_User_</code> <em>model with a</em> <code>_username_</code> <em>and an</em> <code>_email_</code> <em>attribute:</em></p>
</blockquote>
<p>const { Sequelize, DataTypes } = require('sequelize');</p>
<p>const sequelize = new Sequelize('database', 'username', 'password', {<br>
host: 'localhost',<br>
dialect: 'postgres',<br>
});</p>
<p>const User = sequelize.define('User', {<br>
username: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
email: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
});</p>
<blockquote>
<p><em>In this example, we’re defining a</em> <code>_User_</code> <em>model with two attributes:</em> <code>_username_</code> <em>of type</em> <code>_STRING_</code> <em>and</em> <code>_email_</code> <em>of type</em> <code>_STRING_</code><em>, both of which are required (</em><code>_allowNull: false_</code><em>).</em></p>
</blockquote>
<blockquote>
<p><em>Sequelize’s DataTypes and model definition make it easy to work with databases using JavaScript, abstracting away the complexities of raw SQL queries.</em></p>
</blockquote>
<hr>
<h3><strong>Using Associations</strong></h3>
<blockquote>
<p><em>In the context of the above example we have used the models in the following snippet</em></p>
</blockquote>
<p>(async () => {<br>
try {<br>
// Create a user with associated posts<br>
const user = await User.create({<br>
username: 'john_doe',<br>
email: '<a href="mailto:john@example.com">john@example.com</a>',<br>
Posts: [<br>
{<br>
title: 'First Post',<br>
content: 'This is the content of the first post.',<br>
},<br>
{<br>
title: 'Second Post',<br>
content: 'This is the content of the second post.',<br>
},<br>
],<br>
}, {<br>
include: Post,<br>
});</p>
<pre><code>// Find a user and retrieve their posts  
const foundUser = await User.findOne({  
  where: { username: 'john\_doe' },  
  include: Post,  
});  

console.log('User with associated posts:', foundUser.toJSON());  

// Find a post and retrieve its user  
const foundPost = await Post.findOne({  
  where: { title: 'First Post' },  
  include: User,  
});  

console.log('Post with associated user:', foundPost.toJSON());  
</code></pre>
<p>} catch (error) {<br>
console.error('Error:', error);<br>
} finally {<br>
await sequelize.close();<br>
}<br>
})();</p>
<hr>
<h3><strong>Raw Queries</strong></h3>
<blockquote>
<p><em>Raw queries refer to the use of direct SQL statements in your code to interact with a database, bypassing the abstractions provided by Object-Relational Mapping (ORM) libraries like Sequelize. While using an ORM like Sequelize is often preferred for its ease of use, raw queries can be useful in certain situations where complex queries or optimizations are required.Here’s an overview of raw queries, including when and how to use them:</em></p>
</blockquote>
<blockquote>
<p><strong><em>When to Use Raw Queries:</em></strong> _You might consider using raw queries in the following scenarios:<br>
_<strong><em>Performance Optimization:</em></strong> <em>In cases where a specific query optimization is needed, raw queries can be used to fine-tune database performance.</em><strong><em>Advanced Queries:</em></strong> <em>For complex queries that are difficult to express using an ORM’s query builder methods.</em><strong><em>Stored Procedures:</em></strong> <em>When you need to call database-stored procedures directly.</em><strong><em>Database-Specific Features:</em></strong> <em>For utilizing database-specific features that are not supported by the ORM.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Using Raw Queries with Sequelize</em></strong></p>
</blockquote>
<blockquote>
<p><em>Sequelize allows you to execute raw SQL queries using the</em> <code>_sequelize.query()_</code> <em>method. Here's how you can use it:</em></p>
</blockquote>
<p>const { Sequelize } = require('sequelize');</p>
<p>const sequelize = new Sequelize('database', 'username', 'password', {<br>
host: 'localhost',<br>
dialect: 'postgres',<br>
});</p>
<p>async function runRawQuery() {<br>
try {<br>
const results = await sequelize.query('SELECT * FROM users WHERE age > :age', {<br>
replacements: { age: 25 }, // Bind parameters<br>
type: Sequelize.QueryTypes.SELECT,<br>
});</p>
<pre><code>console.log('Raw Query Results:', results);  
</code></pre>
<p>} catch (error) {<br>
console.error('Error executing raw query:', error);<br>
} finally {<br>
await sequelize.close();<br>
}<br>
}</p>
<p>runRawQuery();</p>
<blockquote>
<p><em>In the example above, we’re using</em> <code>_sequelize.query()_</code> <em>to execute a raw SQL query that retrieves users older than a specified age. The</em> <code>_replacements_</code> <em>option is used to bind parameters, enhancing security by preventing SQL injection.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Preventing SQL Injection:</em></strong>_When using raw queries, it’s important to properly sanitize and validate user inputs to prevent SQL injection attacks. Sequelize’s parameter binding, as demonstrated in the example above, helps mitigate this risk.<br>
_<strong><em>Cautions and Considerations:</em></strong> _Raw queries can make your code less portable across different database systems. They can bypass some of the built-in validations and features of the ORM. Use raw queries judiciously and consider whether an ORM’s query builder can fulfill your requirements.<br>
_<strong><em>Escaping Identifiers:</em></strong> <em>If you need to use raw identifiers (table/column names) in your SQL queries, you can use the</em> <code>_sequelize.literal()_</code> <em>function to safely escape them:</em></p>
</blockquote>
<p>sequelize.query(`SELECT * FROM ${sequelize.literal('Users')} WHERE age > :age`, {<br>
replacements: { age: 25 },<br>
type: Sequelize.QueryTypes.SELECT,<br>
});</p>
<hr>
<h3><strong>Indexing</strong></h3>
<blockquote>
<p><em>Indexing is a database optimization technique that improves the speed of data retrieval operations (such as searching, sorting, and filtering) by creating a data structure that allows for more efficient access to the data. In relational databases like PostgreSQL, indexing plays a crucial role in improving query performance.</em></p>
</blockquote>
<blockquote>
<p><em>In the context of Sequelize and PostgreSQL, you can create indexes on specific columns of your tables to speed up queries involving those columns. Here’s how you can work with indexing in Sequelize:</em></p>
</blockquote>
<blockquote>
<p><strong><em>Creating Indexes:</em></strong> <em>In Sequelize, you can define indexes when defining your model using the</em> <code>_indexes_</code> <em>option. Indexes can be created on single or multiple columns. Here's an example of creating an index on the</em> <code>_email_</code> <em>column of the</em> <code>_User_</code> <em>model:</em></p>
</blockquote>
<p>const { Sequelize, DataTypes } = require('sequelize');</p>
<p>const sequelize = new Sequelize('database', 'username', 'password', {<br>
host: 'localhost',<br>
dialect: 'postgres',<br>
});</p>
<p>const User = sequelize.define('User', {<br>
username: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
email: {<br>
type: DataTypes.STRING,<br>
allowNull: false,<br>
},<br>
}, {<br>
indexes: [<br>
{<br>
unique: true,<br>
fields: ['email'], // Index on the 'email' column<br>
}<br>
]<br>
});</p>
<blockquote>
<p><em>In this example, we’re creating a unique index on the</em> <code>_email_</code> <em>column. Unique indexes ensure that no two rows have the same value in the indexed column.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Using Existing Indexes:</em></strong> <em>If you have existing indexes in your database, Sequelize can use them for query optimization. Sequelize will automatically use the indexes when executing queries that involve indexed columns.</em></p>
</blockquote>
<blockquote>
<p><strong><em>Custom Index Names:</em></strong> <em>By default, Sequelize generates index names based on the table name and indexed columns. You can specify custom index names using the</em> <code>_name_</code> <em>property in the</em> <code>_indexes_</code> <em>array:</em></p>
</blockquote>
<p>indexes: [<br>
{<br>
unique: true,<br>
fields: ['email'],<br>
name: 'custom_email_index_name'<br>
}<br>
]</p>
<blockquote>
<p><strong><em>Composite Indexes:</em></strong> <em>You can create composite indexes by specifying multiple columns in the</em> <code>_fields_</code> <em>array:</em></p>
</blockquote>
<p>indexes: [<br>
{<br>
fields: ['column1', 'column2']<br>
}<br>
]</p>
<blockquote>
<p><strong><em>Removing Indexes:</em></strong> <em>To remove an index, you can use the</em> <code>_sequelize.drop_</code> <em>method with the</em> <code>_options_</code> <em>parameter:</em></p>
</blockquote>
<p>User.drop({<br>
indexes: [<br>
'custom_email_index_name', // Name of the index to drop<br>
]<br>
});</p>
<blockquote>
<p>_Keep in mind that dropping indexes will result in a change to your database schema, so use caution.<br>
_<strong><em>Indexes</em></strong> <em>can significantly improve query performance, especially on large datasets. However, overindexing (creating too many unnecessary indexes) can lead to performance issues during data updates and inserts. Therefore, it’s essential to carefully consider which columns should be indexed based on the types of queries your application performs most frequently.Always monitor query performance and database usage when using indexes, and be prepared to adjust your indexing strategy as needed.</em></p>
</blockquote>
<hr>
<h3><strong>Widely used Sequelize Function</strong></h3>
<blockquote>
<p><code>_sequelize.define_</code><em>: Defines a new model with attributes and options.<br>
<em><code>sequelize.sync()</code>: Synchronize model definitions with the database.<br>
<code>_belongsTo_</code></em>: Creates an association between two models, where one model belongs to another.<br>
<em><code>_hasMany_</code></em>: Defines a one-to-many relationship between models.<br>
<em><code>_belongsToMany_</code></em>: Creates a many-to-many association between models.<br>
<em><code>_Model.create_</code></em>: Creates a new instance of a model and inserts it into the database.<br>
<em><code>_Model.findAll_</code></em>: Retrieves all instances of a model from the database.<br>
<em><code>_Model.findByPk_</code></em>: Finds a single instance by its primary key.<br>
<em><code>_Model.update_</code></em>: Updates instances that match the given conditions.<br>
<em><code>_Model.destroy_</code></em>: Deletes instances that match the given conditions.<br>
<em><code>_Model.findOne_</code></em>: Finds the first instance that matches the given conditions.<br>
<em><code>_Model.findAll_</code></em>: Retrieves instances based on specific conditions.<br>
<em><code>_Model.count_</code></em>: Counts the number of instances that match the given conditions.<br>
<em><code>Model.bulkCreate()</code>: Bulk insert multiple records.<br>
<code>_Model.findAll({ order, limit, offset })_</code></em>: Allows sorting and pagination of query results.<br>
<em><code>_sequelize.fn_</code></em>: Provides access to various SQL functions (e.g.,</em> <code>_SUM_</code><em>,</em> <code>_COUNT_</code><em>,</em> <code>_AVG_</code><em>, etc.).<br>
<em><code>_sequelize.col_</code></em>: Represents a column in the database.<br>
<em><code>_sequelize.query_</code></em>: Executes raw SQL queries and returns the results.</em></p>
</blockquote>
<p>There are numerous functions available for individuals to use according to their requirements. If you wish to learn more about how to use sequelize with Postgres, you can refer to the official documentation of Postgres and sequelize for detailed information.</p>
<p>Sequelize: <a href="https://sequelize.org/">https://sequelize.org/</a></p>
<p>Postgres: <a href="https://www.postgresql.org/">https://www.postgresql.org/</a></p>
<p>By Jatin Jain Saraf on August 7, 2023.</p>]]></content:encoded>
      <pubDate>Mon, 07 Aug 2023 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Engineering</category>
    </item>
    <item>
      <title>Clustering In NodeJs</title>
      <link>https://insight.jatinjainsaraf.com/clustering-in-nodejs</link>
      <guid isPermaLink="true">https://insight.jatinjainsaraf.com/clustering-in-nodejs</guid>
      <description>When multiple CPU cores are available, Node.js does not utilize them all by default. Fortunately, Node.js has a native cluster module that…</description>
      <content:encoded><![CDATA[<h1>Clustering In NodeJs</h1>
<p>When multiple CPU cores are available, Node.js does not utilize them all by default. Fortunately, Node.js has a native cluster module that…</p>
<hr>
<h3>Clustering In NodeJs</h3>
<p>When multiple CPU cores are available, Node.js does not utilize them all by default. Fortunately, Node.js has a native cluster module that allows for the creation of child processes (workers) that can run simultaneously while sharing the same server port. Each child process has its own event loop, memory, and V8 instance. These child processes communicate with the main parent Node.js process through interprocess communication. Clusters of Node.js processes can be used to distribute workloads among application threads, or if process isolation is not needed, the module can be used to run multiple application threads within a single Node.js instance. The cluster module makes it easy to create child processes that can share server ports.</p>
<h3>The need for clustering in Node.js</h3>
<p>An instance of Node.js runs on a single thread (you can read more about <a href="https://blog.logrocket.com/a-complete-guide-to-threads-in-node-js-4fa3898fe74f/">threads in Node.js here</a>). The official <a href="https://nodejs.org/en/about/">Node.js “About” page</a> states: “Node.js being designed without threads doesn’t mean you can’t take advantage of multiple cores in your environment.” That’s where it points to the cluster module.</p>
<p>The <a href="https://nodejs.org/api/cluster.html">cluster module doc</a> adds: “To take advantage of multi-core systems, the user will sometimes want to launch a cluster of Node.js processes to handle the load.” So, to take advantage of the multiple processors on the system running Node.js, we should use the cluster module.</p>
<p>Exploiting the available cores to distribute the load between them gives our Node.js app a performance boost. As most modern systems have multiple cores, we should be using the cluster module in Node.js to get the most performance juice out of these newer machines.</p>
<h3>How does the Node.js cluster module work?</h3>
<p>In a nutshell, the Node.js cluster module acts as a load balancer. It distributes a load to the child processes running simultaneously on a shared port. Node.js is not great with blocking code, so if there is only one processor and it’s blocked by a heavy and CPU-intensive operation, other requests are just waiting in the queue for this operation to complete.</p>
<p>With multiple processes, if one process is busy with a relatively CPU-intensive operation, other processes can take up the other requests coming in and utilize the other CPUs/cores available. This is the power of the cluster module — workers share the load and the app does not stop.</p>
<p>The master process can distribute the load to the child process in two ways. The first (and default) is a round-robin fashion. The second way is the master process listens to a socket and sends the work to interested workers. The workers then process the incoming requests.</p>
<p>However, the second method is not super clear and easy to comprehend like the basic round-robin approach.</p>
<h3>Prerequisites</h3>
<p>To follow this guide about Node.js clustering, you should have the following:</p>
<ul>
<li>Node.js running on your machine, the latest LTS is advisable. It is Node.js 18 at the time of writing.</li>
<li>Working knowledge of Node.js and Express</li>
<li>Basic knowledge of how processes and threads work</li>
<li>Working knowledge of Git and GitHub</li>
</ul>
<h3><strong>Implementation &#x26; Configuration</strong></h3>
<p>import cluster from 'node:cluster';<br>
import http from 'node:http';<br>
import { availableParallelism } from 'node:os';<br>
import process from 'node:process';</p>
<p>const numCPUs = availableParallelism();</p>
<p>if (cluster.isPrimary) {<br>
console.log(`Primary ${process.pid} is running`);</p>
<p>// Fork workers.<br>
for (let i = 0; i &#x3C; numCPUs; i++) {<br>
cluster.fork();<br>
}</p>
<p>cluster.on('exit', (worker, code, signal) => {<br>
console.log(`worker ${worker.process.pid} died`);<br>
});<br>
} else {<br>
// Workers can share any TCP connection<br>
// In this case it is an HTTP server<br>
http.createServer((req, res) => {<br>
res.writeHead(200);<br>
res.end('hello world\n');<br>
}).listen(8000);</p>
<p>console.log(`Worker ${process.pid} started`);<br>
}</p>
<p>Running Node.js will now share port 8000 between the workers:</p>
<p>$ node server.js<br>
Primary 3596 is running<br>
Worker 4324 started<br>
Worker 4520 started<br>
Worker 6056 started<br>
Worker 5644 started</p>
<p>Before implementing clustering in your Node.js project, it's crucial to comprehend its advantages and constraints to determine if it's the most suitable option for your application's requirements.</p>
<h3><strong>Benefits of Clustering:</strong></h3>
<ol>
<li>By using clustering, you can make the most of the multiple CPU cores on your machine. This can greatly enhance the speed and efficiency of your application, particularly for tasks that rely heavily on the CPU.</li>
<li>By clustering, your application can effectively handle a greater number of simultaneous requests through the distribution of workload among several worker processes. This results in improved scalability and performance.</li>
<li>In terms of fault tolerance and availability, the ability of the remaining worker processes to handle incoming requests even if one crashes is a significant advantage.</li>
</ol>
<h3><strong>Limitations of Clustering:</strong></h3>
<ol>
<li>Adding clustering to your application code can increase its complexity. You must be careful in managing inter-process communication, handling shared resources, and avoiding potential race conditions.</li>
<li>The usage of memory increases with each worker process, which could be problematic if the server has limited memory resources.</li>
<li>Managing states can become complicated with clustering, as each worker process has its own memory space. If your application heavily depends on in-memory states, you will have to develop methods to synchronize and share states among worker processes.</li>
<li>Clustering may not always be necessary for applications that are I/O-bound, such as network requests or database queries. This is because the bottleneck often lies in the I/O operations rather than CPU processing, making clustering less beneficial in these cases.</li>
</ol>
<h3><strong>When to Use Clustering:</strong></h3>
<p>If your application is CPU-bound and receives high requests, clustering can be a valuable solution. It is particularly useful for tasks that involve complex computations, image processing, video encoding, and specific types of data analysis. By distributing the workload, clustering can enhance overall efficiency and help overcome any limitations to performance caused by CPU utilization.</p>
<p>By Jatin Jain Saraf on August 7, 2023.</p>]]></content:encoded>
      <pubDate>Mon, 07 Aug 2023 00:00:00 GMT</pubDate>
      <author>j.saraf@supra.com (Jatin Jain Saraf)</author>
      <category>Node.js</category>
      <category>Backend</category>
      <category>Performance</category>
    </item>
  </channel>
</rss>