<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Aaron Yong - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Aaron Yong - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 26 May 2026 04:42:58 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/aaronhsyong/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How AI Changed the Economics of Writing Clean Code ]]>
                </title>
                <description>
                    <![CDATA[ If you've ever wanted to add an interface to a codebase and gotten pushback, you already know the argument: "That's twice the code for the same thing." And honestly? It was a fair point. You'd write t ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-ai-changed-the-economics-of-writing-clean-code/</link>
                <guid isPermaLink="false">69f0bce210a70b3335bf635a</guid>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Code Quality ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ best practices ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aaron Yong ]]>
                </dc:creator>
                <pubDate>Tue, 28 Apr 2026 13:57:54 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ecb13bda-70dd-437a-8d9a-4ef8b18ccc05.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've ever wanted to add an interface to a codebase and gotten pushback, you already know the argument: "That's twice the code for the same thing."</p>
<p>And honestly? It was a fair point. You'd write the contract — the interface, the abstract class, the protocol — and then write the implementation. Two files where one would do. That's more surface area, more indirection, and more to maintain.</p>
<p>The Ruby and Rails communities built an entire philosophy around this: convention over configuration, less ceremony, fewer keystrokes. If the framework could infer your intent, why spell it out?</p>
<p>Then AI happened.</p>
<p>I was recently chatting with a CEO about what current-generation software engineers get wrong, and he put it cleanly:</p>
<blockquote>
<p>"Abstract interfaces were challenging a few months ago just because it required twice as much code. But with AI, lines of code are free. The reason we still need such constructs is because at some point a human still needs to look at the code. Interfaces reduce the cognitive load."</p>
</blockquote>
<p>That framing stuck with me. The cost of writing code has collapsed. The cost of reading it hasn't moved. And that asymmetry changes everything about how you should think about abstraction.</p>
<p>Here's what I mean.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-your-brain-is-the-bottleneck">Your Brain Is the Bottleneck</a></p>
</li>
<li><p><a href="#heading-the-greats-already-knew-this">The Greats Already Knew This</a></p>
</li>
<li><p><a href="#heading-the-economics-have-flipped">The Economics Have Flipped</a></p>
</li>
<li><p><a href="#heading-the-data-backs-it-up">The Data Backs It Up</a></p>
</li>
<li><p><a href="#heading-the-contrarian-case-and-why-it-actually-agrees">The Contrarian Case (And Why It Actually Agrees)</a></p>
</li>
<li><p><a href="#heading-what-this-means-for-you">What This Means for You</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-your-brain-is-the-bottleneck">Your Brain Is the Bottleneck</h2>
<p>This isn't a vibes argument. There's actual neuroscience behind why interfaces help.</p>
<p>In 1988, educational psychologist John Sweller introduced Cognitive Load Theory. A <a href="https://dl.acm.org/doi/full/10.1145/3483843">2022 ACM review</a> covers how it's been applied to computing education since.</p>
<p>The short version: your brain juggles three types of load when processing information. <em>Intrinsic</em> load is the inherent difficulty of the problem itself. <em>Extraneous</em> load is the noise — poorly organized information, unnecessary details, bad naming. <em>Germane</em> load is the good stuff — the mental effort you spend building useful mental models.</p>
<p>Here's the kicker: your working memory can only hold a handful of chunks of information at a time — cognitive scientists typically estimate somewhere between 2 and 6. Not 2 to 6 files, or 2 to 6 classes — 2 to 6 <em>things</em>.</p>
<p>Felienne Hermans explores this in <em>The Programmer's Brain</em> (2021), arguing that design patterns act as chunking aids. When you recognize a Strategy pattern, your brain collapses an entire class hierarchy into a single cognitive unit. The word "Strategy" replaces five classes and their relationships. That's not hand-waving about clean code — that's how human memory actually works.</p>
<p>And we can literally see it on brain scans. In 2021, a team led by Norman Peitek and Janet Siegmund published <a href="https://dl.acm.org/doi/10.1109/ICSE43902.2021.00056">an fMRI study on program comprehension</a> that won the ACM SIGSOFT Distinguished Paper Award at ICSE.</p>
<p>They put developers in brain scanners and watched what happened when they read code. The finding: semantic-level comprehension — understanding <em>what</em> code does — required measurably less neural activation than bottom-up syntactic parsing — tracing <em>how</em> it does it.</p>
<p>An interface lets you comprehend at the semantic level. <code>UserRepository.findById(id)</code> tells you everything you need to know without opening the implementation. Your brain doesn't need to hold the SQL query, the connection pool logic, the error handling, and the result mapping in working memory simultaneously. The interface compresses all of that into one chunk.</p>
<p>That's not elegance. That's neuroscience.</p>
<h2 id="heading-the-greats-already-knew-this">The Greats Already Knew This</h2>
<p>The case for abstraction isn't new. The people who built the foundations of computer science were making this argument before most of us were born.</p>
<p>Dijkstra said it with precision:</p>
<blockquote>
<p><em>"The purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise."</em></p>
</blockquote>
<p>Abstraction isn't about hiding things from people who can't handle complexity. It's about creating a level of discourse where you can reason clearly.</p>
<p>David Parnas formalized information hiding in his <a href="https://dl.acm.org/doi/10.1145/361598.361623">1972 ACM paper</a>: <em>"Every module is characterized by its knowledge of a design decision which it hides from all others."</em> He proved that decomposing systems by design decisions (rather than processing steps) produced modules that were both more flexible <em>and</em> easier to understand. Comprehensibility wasn't a bonus — it was the design criterion.</p>
<p>Tony Hoare argued that abstraction is the most powerful tool available to the human intellect — a way to manage complexity by focusing on what matters and ignoring what doesn't. Martin Fowler brought it down to earth:</p>
<blockquote>
<p><em>"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."</em></p>
</blockquote>
<p>And then there's John Ousterhout, whose book <em>A Philosophy of Software Design</em> (2018) makes the connection to cognitive load explicit. His central argument: more lines of code can actually be <em>simpler</em> if they reduce cognitive load.</p>
<p>His concept of <em>deep modules</em> — simple interfaces hiding complex implementations — is essentially the argument that interfaces are worth their weight in code. The Unix file system API (<code>open</code>, <code>close</code>, <code>read</code>, <code>write</code>, <code>lseek</code>) is five functions hiding an enormous amount of complexity. That's a deep module. That's the goal.</p>
<p>The Gang of Four put it first in their book for a reason. Page one: <em>"Program to an interface, not an implementation."</em></p>
<p>None of this is controversial. But it's easy to forget when your AI tool just generated 200 lines of perfectly functional inline code in three seconds.</p>
<h2 id="heading-the-economics-have-flipped">The Economics Have Flipped</h2>
<p>Here's where the CEO's insight becomes an economic argument.</p>
<p>The historical case against interfaces was always about <em>writing cost</em>. Interfaces meant more code to write, more files to create, more boilerplate to maintain. The entire dynamic typing movement — Python, Ruby, JavaScript — was partly a reaction to the ceremony that languages like Java imposed. Convention over configuration. Don't Repeat Yourself. Less is more.</p>
<p>But ask yourself: what exactly is the cost of writing boilerplate now?</p>
<p>GitHub's <a href="https://arxiv.org/abs/2302.06590">2022 controlled study</a> found that developers using Copilot completed tasks 55% faster. The boilerplate that used to justify skipping interfaces — the extra file, the type definitions, the method signatures — takes seconds to generate. The writing cost of an interface has effectively collapsed to zero.</p>
<p>But again, the reading cost hasn't budged.</p>
<p>Robert C. Martin argued in <em>Clean Code</em> (2008) that developers spend far more time reading code than writing it — an observation he framed as a ratio of 10 to 1.</p>
<p>You can quibble with the exact number (it's anecdotal), but the direction is consistent across studies. A <a href="https://ieeexplore.ieee.org/document/7997917/">large-scale field study</a> tracking 78 professional developers across 3,148 working hours found they spend roughly 58% of their time on program comprehension alone. New developer onboarding averages six weeks — most of which is spent understanding existing systems, not producing new ones.</p>
<p>Addy Osmani named this asymmetry perfectly. In a <a href="https://addyosmani.com/blog/comprehension-debt/">March 2026 piece</a>, he described <em>comprehension debt</em>:</p>
<blockquote>
<p>"When a developer on your team writes code, the human review process has always been a bottleneck — but a productive and educational one. Reading their PR forces comprehension. AI-generated code breaks that feedback loop. The volume is too high."</p>
</blockquote>
<p>The output looks clean, passes linting, follows conventions — precisely the signals that historically triggered merge confidence. But comprehension debt is distinct from technical debt because it accumulates invisibly — your velocity metrics, your DORA scores, your PR counts all look fine while your team's actual understanding of the codebase quietly erodes.</p>
<p>So here's the math: AI reduced the cost of writing abstractions to near zero. The cost of <em>not</em> having them — in human reading time, onboarding friction, and comprehension debt — hasn't changed at all. The break-even point for "is this interface worth it?" just shifted massively in favor of "yes."</p>
<h2 id="heading-the-data-backs-it-up">The Data Backs It Up</h2>
<p>This isn't theoretical. We have data on what happens when AI generates code without good abstractions.</p>
<p><a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">GitClear analyzed 211 million changed lines of code</a> between 2020 and 2024. Their findings: code churn — lines reverted or updated within two weeks — doubled compared to the pre-AI baseline. Copy-pasted code blocks rose from 8.3% to 12.3%. And refactoring-associated changes dropped from 25% to under 10%.</p>
<p>AI-generated code, as they put it, "resembles an itinerant contributor, prone to violate the DRY-ness of the repos visited."</p>
<p>The <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">METR study</a> (2025) found something even more striking. Experienced open-source developers <em>predicted</em> AI would make them 24% faster. They <em>perceived</em> being 20% faster while using it. They were actually 19% slower. The perception gap is the story — you <em>feel</em> productive while generating code that creates more work downstream.</p>
<p>And then there's a study from Anthropic (yes, the company that makes Claude — full disclosure). They observed 52 software engineers learning a new library. The AI-assisted group completed tasks at the same speed, but scored <a href="https://arxiv.org/abs/2601.20245">17% lower on comprehension quizzes</a> afterward — 50% versus 67%. The biggest declines were in debugging ability. You can ship code you don't understand. You can't debug code you don't understand.</p>
<p>Kent Beck <a href="https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0">put it bluntly</a>: "The value of 90% of my skills just dropped to $0. The leverage for the remaining 10% went up 1000x." What that remaining 10% is, he leaves deliberately open — but it's hard to read that and not think about system design.</p>
<h2 id="heading-the-contrarian-case-and-why-it-actually-agrees">The Contrarian Case (And Why It Actually Agrees)</h2>
<p>I'd be dishonest if I didn't address the people who argue against abstraction. And some of them are very smart.</p>
<p>Casey Muratori's <a href="https://www.computerenhance.com/p/clean-code-horrible-performance">"Clean Code, Horrible Performance"</a> demonstrated that polymorphism and virtual dispatch can make code 10 to 15 times slower than straightforward procedural alternatives.</p>
<p>His benchmark is real. If you're writing a game engine or a high-frequency trading system, abstract interfaces on your hot path will cost you.</p>
<p>Dan Abramov wrote <a href="https://overreacted.io/goodbye-clean-code/">"Goodbye, Clean Code"</a> after watching a premature abstraction make his codebase harder to modify:</p>
<blockquote>
<p><em>"My code traded the ability to change requirements for reduced duplication, and it was not a good trade."</em></p>
</blockquote>
<p>Sandi Metz <a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction">put it more sharply</a>: <em>"Duplication is far cheaper than the wrong abstraction."</em></p>
<p>And Rich Hickey, in his talk <a href="https://www.infoq.com/presentations/Simple-Made-Easy/">"Simple Made Easy"</a>, draws the critical distinction: <em>simple</em> (not intertwined) is not the same as <em>easy</em> (familiar). Wrong abstractions <em>complect</em> — they braid concerns together rather than separating them.</p>
<p>Here's the thing: none of these are arguments against abstraction. They're arguments against <em>bad</em> abstraction.</p>
<p>Muratori's performance argument applies to hot paths in performance-critical systems — not to your REST API's service layer. Abramov and Metz argue against <em>premature</em> abstraction — pulling patterns out before you understand the domain. And Hickey's entire talk is a case <em>for</em> the right abstractions, the ones that genuinely decompose rather than complect.</p>
<p>The irony is that in an AI-assisted world, these arguments are <em>easier</em> to address. You can generate the explicit, unabstracted version first. Let it stabilize. Watch the patterns emerge. Then extract the abstraction — with AI handling the mechanical refactoring. The cost of the "duplicate first, abstract later" approach just dropped to near zero.</p>
<h2 id="heading-what-this-means-for-you">What This Means for You</h2>
<p>If you're writing code with AI tools — and at this point, <a href="https://survey.stackoverflow.co/2024/ai">most of us are</a> — the temptation is to let the AI produce whatever it produces and move on. It works. It passes the tests. Ship it.</p>
<p>But "it works" is table stakes. The harder question is: can the next person who opens this code understand it in under five minutes? Can <em>you</em> understand it in six months?</p>
<p>Interfaces aren't about making code prettier or satisfying some abstract (pun intended) design principle. They're compression algorithms for human cognition. They let your brain operate at the semantic level instead of the syntactic level. And now that AI has eliminated the only real cost of creating them — the boilerplate — there's no economic argument left for skipping them.</p>
<p>The rules haven't changed. The excuse has just expired.</p>
<h2 id="heading-references">References</h2>
<h3 id="heading-academic-papers">Academic Papers</h3>
<ul>
<li><p>Duran, R., Zavgorodniaia, A., &amp; Sorva, J. (2022). <a href="https://dl.acm.org/doi/full/10.1145/3483843">"Cognitive Load Theory in Computing Education Research: A Review."</a> <em>ACM Transactions on Computing Education, 22</em>(4), Article 40.</p>
</li>
<li><p>Parnas, D.L. (1972). <a href="https://dl.acm.org/doi/10.1145/361598.361623">"On the Criteria To Be Used in Decomposing Systems into Modules."</a> <em>Communications of the ACM, 15</em>(12), 1053–1058.</p>
</li>
<li><p>Peitek, N., Apel, S., Parnin, C., Brechmann, A., &amp; Siegmund, J. (2021). <a href="https://dl.acm.org/doi/10.1109/ICSE43902.2021.00056">"Program Comprehension and Code Complexity Metrics: An fMRI Study."</a> <em>ICSE 2021</em>. ACM SIGSOFT Distinguished Paper Award.</p>
</li>
<li><p>Peng, S., Kalliamvakou, E., Cihon, P., &amp; Demirer, M. (2023). <a href="https://arxiv.org/abs/2302.06590">"The Impact of AI on Developer Productivity: Evidence from GitHub Copilot."</a> <em>arXiv:2302.06590</em>.</p>
</li>
<li><p>Shen, J.H. &amp; Tamkin, A. (2026). <a href="https://arxiv.org/abs/2601.20245">"How AI Impacts Skill Formation."</a> <em>arXiv:2601.20245</em>.</p>
</li>
<li><p>Xia, X., Bao, L., Lo, D., Xing, Z., Hassan, A.E., &amp; Li, S. (2018). <a href="https://ieeexplore.ieee.org/document/7997917/">"Measuring Program Comprehension: A Large-Scale Field Study with Professionals."</a> <em>IEEE Transactions on Software Engineering, 44</em>(10), 951–976.</p>
</li>
<li><p>METR. (2025). <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">"Measuring the Impact of Early 2025 AI on Experienced Open Source Developer Productivity."</a> <em>metr.org</em>.</p>
</li>
</ul>
<h3 id="heading-talks-and-blog-posts">Talks and Blog Posts</h3>
<ul>
<li><p>Hickey, R. (2011). <a href="https://www.infoq.com/presentations/Simple-Made-Easy/">"Simple Made Easy."</a> <em>Strange Loop Conference</em>.</p>
</li>
<li><p>Beck, K. (2023). <a href="https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0">"90% of My Skills Are Now Worth $0."</a> <em>Tidy First? Substack</em>.</p>
</li>
<li><p>Osmani, A. (2026). <a href="https://addyosmani.com/blog/comprehension-debt/">"Comprehension Debt: The Hidden Cost of AI-Generated Code."</a> <em>addyosmani.com</em>.</p>
</li>
<li><p>Muratori, C. (2023). <a href="https://www.computerenhance.com/p/clean-code-horrible-performance">"Clean Code, Horrible Performance."</a> <em>Computer Enhance</em>.</p>
</li>
<li><p>Abramov, D. (2020). <a href="https://overreacted.io/goodbye-clean-code/">"Goodbye, Clean Code."</a> <em>overreacted.io</em>.</p>
</li>
<li><p>Metz, S. (2016). <a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction">"The Wrong Abstraction."</a> <em>sandimetz.com</em>.</p>
</li>
<li><p>GitClear. (2025). <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">"AI Assistant Code Quality in 2025."</a> <em>gitclear.com</em>.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use PostgreSQL as a Cache, Queue, and Search Engine ]]>
                </title>
                <description>
                    <![CDATA[ "Just use Postgres" has been circulating as advice for years, but most articles arguing for it are opinion pieces. I wanted hard numbers. So I built a benchmark suite that pits vanilla PostgreSQL agai ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-postgresql-as-a-cache-queue-and-search-engine/</link>
                <guid isPermaLink="false">69e7accfe43672781470ff97</guid>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ database ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ performance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Databases ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aaron Yong ]]>
                </dc:creator>
                <pubDate>Tue, 21 Apr 2026 16:58:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6fcdd3c0-eead-42a7-b2f0-cf4c6a3d06dc.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>"Just use Postgres" has been circulating as advice for years, but most articles arguing for it are opinion pieces. I wanted hard numbers.</p>
<p>So I built a benchmark suite that pits vanilla PostgreSQL against a feature-optimized PostgreSQL instance — measuring caching, message queues, full-text search, and pub/sub under controlled conditions.</p>
<p>In this article, you'll learn how to use PostgreSQL's built-in features for caching, job queues, full-text search, and pub/sub. You'll see actual benchmark results (latency percentiles, throughput, and error rates) comparing naive PostgreSQL patterns against optimized ones, and understand where PostgreSQL's limits are so you can decide whether you really need that extra service in your stack.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-setup">The Setup</a></p>
</li>
<li><p><a href="#heading-benchmark-1-caching-with-unlogged-tables">Benchmark 1: Caching with UNLOGGED Tables</a></p>
</li>
<li><p><a href="#heading-benchmark-2-job-queues-with-skip-locked">Benchmark 2: Job Queues with SKIP LOCKED</a></p>
</li>
<li><p><a href="#heading-benchmark-3-full-text-search-with-tsvector">Benchmark 3: Full-Text Search with tsvector</a></p>
</li>
<li><p><a href="#heading-benchmark-4-pubsub-with-listennotify">Benchmark 4: Pub/Sub with LISTEN/NOTIFY</a></p>
</li>
<li><p><a href="#heading-the-combined-workload-the-honest-test">The Combined Workload: The Honest Test</a></p>
</li>
<li><p><a href="#heading-what-i-learned">What I Learned</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along or reproduce the benchmarks, you'll need:</p>
<ul>
<li><p>Docker and Docker Compose</p>
</li>
<li><p>Node.js 20+ (for the Express TypeScript API layer)</p>
</li>
<li><p><a href="https://k6.io/">k6</a> for load testing</p>
</li>
<li><p>Basic familiarity with SQL and PostgreSQL</p>
</li>
</ul>
<p>The full benchmark project is <a href="https://github.com/aaronhsyong2/pg-stack-benchmark">open source on GitHub</a> — you can clone it and run every test yourself.</p>
<h2 id="heading-the-setup">The Setup</h2>
<p>The benchmark uses two identical PostgreSQL 17 instances running in Docker containers, each with fixed resource constraints (2 CPUs, 2 GB RAM). Both share the same Express TypeScript API layer — the only difference is which PostgreSQL features are enabled.</p>
<pre><code class="language-plaintext">┌─────────┐     ┌──────────────────┐     ┌─────────────────┐
│   k6    │────&gt;│  Express API     │────&gt;│  PG Baseline    │
│  (load  │     │  (TypeScript)    │     │  (vanilla PG17) │
│  test)  │────&gt;│  Port 3001/3002  │────&gt;│  PG Modded      │
└─────────┘     └──────────────────┘     │  (features on)  │
                                         └─────────────────┘
</code></pre>
<p>The baseline instance uses naïve approaches (regular tables, <code>ILIKE</code> search, polling). The modded instance uses PostgreSQL's built-in features (UNLOGGED tables, <code>tsvector</code> with GIN indexes, <code>LISTEN/NOTIFY</code>, partial indexes). Same hardware, same API code, same data. Only the database features differ.</p>
<p>Both instances share this tuned <code>postgresql.conf</code>:</p>
<pre><code class="language-ini"># Memory allocation
shared_buffers = 512MB           # 25% of available RAM
effective_cache_size = 1536MB    # 75% of RAM — helps the query planner
work_mem = 16MB                  # per-sort/hash operation memory

# SSD-optimized planner settings
random_page_cost = 1.1           # default 4.0 assumes spinning disks
effective_io_concurrency = 200   # allow parallel I/O on SSDs
</code></pre>
<p>These settings matter. The defaults assume spinning disks from the early 2000s. Setting <code>random_page_cost = 1.1</code> tells the query planner that random reads are nearly as fast as sequential reads on SSDs, which encourages index usage over sequential scans.</p>
<h2 id="heading-benchmark-1-caching-with-unlogged-tables">Benchmark 1: Caching with UNLOGGED Tables</h2>
<p><strong>The idea:</strong> Use an UNLOGGED table as an in-database cache. UNLOGGED tables skip PostgreSQL's Write-Ahead Log (WAL) — the mechanism that guarantees durability. Since cache data is ephemeral by nature, losing it on a crash is acceptable, and skipping WAL removes the biggest write bottleneck.</p>
<pre><code class="language-sql">-- Modded: UNLOGGED table for cache entries
CREATE UNLOGGED TABLE cache_entries (
    key TEXT PRIMARY KEY,
    value JSONB NOT NULL,
    expires_at TIMESTAMPTZ
);

-- Baseline: same schema, but a regular (logged) table
CREATE TABLE cache_entries (
    key TEXT PRIMARY KEY,
    value JSONB NOT NULL,
    expires_at TIMESTAMPTZ
);
</code></pre>
<h3 id="heading-results-200-virtual-users">Results (200 Virtual Users)</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>p50</th>
<th>p95</th>
<th>avg</th>
<th>req/s</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline (regular table)</td>
<td>1.87ms</td>
<td>6.00ms</td>
<td>2.50ms</td>
<td>1,754/s</td>
</tr>
<tr>
<td>Modded (UNLOGGED table)</td>
<td>1.71ms</td>
<td>5.24ms</td>
<td>2.17ms</td>
<td>1,760/s</td>
</tr>
</tbody></table>
<p>A consistent 13% improvement across all percentiles. Not dramatic, but free — you change one keyword in your <code>CREATE TABLE</code> statement.</p>
<h3 id="heading-under-stress-1000-virtual-users-no-sleep">Under Stress (1,000 Virtual Users, No Sleep)</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>p50</th>
<th>p95</th>
<th>req/s</th>
<th>Total Requests</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline</td>
<td>83.38ms</td>
<td>143.23ms</td>
<td>7,663/s</td>
<td>728,021</td>
</tr>
<tr>
<td>Modded</td>
<td>77.69ms</td>
<td>126.39ms</td>
<td>8,062/s</td>
<td>765,934</td>
</tr>
</tbody></table>
<p>The relative improvement stays locked at 12-13% regardless of load level. The UNLOGGED advantage is a per-write optimization — it saves the same amount of I/O whether you are doing 100 or 10,000 writes per second. The modded instance served 37,000 more requests in the same time window.</p>
<h3 id="heading-the-verdict">The Verdict</h3>
<p>UNLOGGED tables won't match Redis for sub-millisecond hot-path caching (real-time bidding, gaming leaderboards). But for web applications where the difference between 2ms and 5ms is invisible to users, they eliminate an entire infrastructure dependency for zero additional complexity.</p>
<p>You do give up Redis data structures (sorted sets, HyperLogLog, streams). If you need those, a dedicated cache is still the right call.</p>
<h2 id="heading-benchmark-2-job-queues-with-skip-locked">Benchmark 2: Job Queues with SKIP LOCKED</h2>
<p><strong>The idea:</strong> Use PostgreSQL as a job queue with <code>SELECT ... FOR UPDATE SKIP LOCKED</code>. Multiple workers poll the same table, and <code>SKIP LOCKED</code> ensures each worker gets a different row — no duplicates, no contention.</p>
<pre><code class="language-sql">-- Queue table with a partial index on pending jobs only
CREATE TABLE job_queue (
    id SERIAL PRIMARY KEY,
    payload JSONB NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Partial index: only indexes pending jobs
-- As jobs complete, they leave the index — it stays small forever
CREATE INDEX idx_pending_jobs ON job_queue (created_at)
    WHERE status = 'pending';
</code></pre>
<p>The dequeue pattern:</p>
<pre><code class="language-sql">-- Atomic dequeue: select + update in one statement
UPDATE job_queue SET status = 'processing'
WHERE id = (
    SELECT id FROM job_queue
    WHERE status = 'pending'
    ORDER BY created_at
    LIMIT 1
    FOR UPDATE SKIP LOCKED  -- skip rows locked by other workers
) RETURNING *;
</code></pre>
<p>How <code>SKIP LOCKED</code> works: Worker A locks row 1. Worker B tries row 1, sees the lock, skips it, and takes row 2 instead. No blocking, no duplicates. If a worker crashes, the transaction rolls back and the row becomes available again.</p>
<h3 id="heading-results-100-producers-50-consumers">Results (100 Producers + 50 Consumers)</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>p50</th>
<th>p95</th>
<th>avg</th>
<th>req/s</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline (full index)</td>
<td>1.90ms</td>
<td>5.01ms</td>
<td>2.30ms</td>
<td>1,053/s</td>
</tr>
<tr>
<td>Modded (partial index)</td>
<td>1.81ms</td>
<td>5.28ms</td>
<td>2.29ms</td>
<td>1,052/s</td>
</tr>
</tbody></table>
<p>They're virtually identical. The partial index doesn't show its value in a 60-second benchmark because the table doesn't accumulate enough completed rows for the index size difference to matter. In a production system with millions of completed jobs, the partial index keeps the index at kilobytes while a full index grows to gigabytes.</p>
<h3 id="heading-the-verdict">The Verdict</h3>
<p><code>SKIP LOCKED</code> is production-ready for job queues. Libraries like <a href="https://github.com/timgit/pg-boss">pg-boss</a> (Node.js) and <a href="https://github.com/riverqueue/river">river</a> (Go) build on this exact pattern.</p>
<p>You do give up exchange/routing patterns (fan-out, topic-based routing) and consumer groups with message replay. If you need those, a dedicated message broker is still the right tool. For simple "process this job once" workloads, PostgreSQL handles it.</p>
<h2 id="heading-benchmark-3-full-text-search-with-tsvector">Benchmark 3: Full-Text Search with tsvector</h2>
<p><strong>The idea:</strong> Use PostgreSQL's built-in full-text search instead of a separate search service. A <code>tsvector</code> column stores pre-processed search tokens, and a GIN (Generalized Inverted Index) enables fast lookups using the same inverted index concept that powers Elasticsearch.</p>
<pre><code class="language-sql">-- Search-optimized article table
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title TEXT NOT NULL,
    body TEXT NOT NULL,
    search_vector tsvector  -- pre-computed search tokens
);

-- GIN index for full-text search
CREATE INDEX idx_search ON articles USING GIN (search_vector);

-- Auto-update search_vector on insert/update
CREATE OR REPLACE FUNCTION update_search_vector() RETURNS trigger AS $$
BEGIN
    NEW.search_vector := to_tsvector('english',
        COALESCE(NEW.title, '') || ' ' || COALESCE(NEW.body, ''));
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_search
    BEFORE INSERT OR UPDATE ON articles
    FOR EACH ROW EXECUTE FUNCTION update_search_vector();
</code></pre>
<p>The baseline uses <code>ILIKE</code> with a leading wildcard — the approach most developers reach for first:</p>
<pre><code class="language-sql">-- Baseline: sequential scan on every query
SELECT * FROM articles
WHERE title ILIKE '%postgresql%' OR body ILIKE '%postgresql%';

-- Modded: GIN index lookup with relevance ranking
SELECT id, title,
    ts_rank(search_vector, plainto_tsquery('english', 'postgresql')) AS rank
FROM articles
WHERE search_vector @@ plainto_tsquery('english', 'postgresql')
ORDER BY rank DESC LIMIT 20;
</code></pre>
<h3 id="heading-results-500-virtual-users">Results (500 Virtual Users)</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>p50</th>
<th>p95</th>
<th>avg</th>
<th>req/s</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline (ILIKE)</td>
<td>1.96ms</td>
<td>101.83ms</td>
<td>25.22ms</td>
<td>561/s</td>
</tr>
<tr>
<td>Modded (tsvector + GIN)</td>
<td>2.76ms</td>
<td>10.39ms</td>
<td>3.76ms</td>
<td>675/s</td>
</tr>
</tbody></table>
<p>This is the standout result. The baseline's p95 of 101ms versus the modded's 10ms is a 10x improvement.</p>
<p>Why the baseline's p50 (1.96ms) is slightly better than the modded's (2.76ms): simple <code>ILIKE</code> queries on small result sets can be fast when the data fits in <code>shared_buffers</code>. But as load increases and the buffer cache is contested, sequential scans degrade dramatically. The GIN index stays stable.</p>
<h3 id="heading-under-stress-500-virtual-users-no-sleep">Under Stress (500 Virtual Users, No Sleep)</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>p50</th>
<th>p95</th>
<th>req/s</th>
<th>Total Requests</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline (ILIKE)</td>
<td>599ms</td>
<td>1,000ms</td>
<td>558/s</td>
<td>50,212</td>
</tr>
<tr>
<td>Modded (tsvector)</td>
<td>209ms</td>
<td>396ms</td>
<td>1,441/s</td>
<td>129,679</td>
</tr>
</tbody></table>
<p>ILIKE collapses to 1-second p95 latencies. Each query forces a sequential scan of all 10,000 articles, blocking shared buffers and starving concurrent queries. The tsvector approach serves 2.6x more requests in the same time window because the GIN index lookup is O(log n) regardless of concurrency.</p>
<h3 id="heading-the-verdict">The Verdict</h3>
<p>This is the strongest argument in the entire benchmark. The fix requires zero extensions — <code>to_tsvector()</code>, <code>plainto_tsquery()</code>, and <code>CREATE INDEX USING GIN</code> are all built into core PostgreSQL. If you're doing <code>WHERE column ILIKE '%term%'</code> on any table with more than a few thousand rows, you're leaving massive performance on the table.</p>
<p>You do give up distributed search across shards, complex analyzers for CJK languages, and aggregation/faceted search pipelines. For a product search bar, blog search, or internal tool — PostgreSQL is enough.</p>
<h2 id="heading-benchmark-4-pubsub-with-listennotify">Benchmark 4: Pub/Sub with LISTEN/NOTIFY</h2>
<p><strong>The idea:</strong> Use PostgreSQL's native <code>LISTEN/NOTIFY</code> for pub/sub messaging, triggered automatically on INSERT via a database trigger.</p>
<pre><code class="language-sql">-- Trigger that fires pg_notify on every new message
CREATE OR REPLACE FUNCTION notify_message() RETURNS trigger AS $$
BEGIN
    PERFORM pg_notify(NEW.channel, NEW.payload::text);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_notify
    AFTER INSERT ON messages
    FOR EACH ROW EXECUTE FUNCTION notify_message();
</code></pre>
<h3 id="heading-results-200-virtual-users">Results (200 Virtual Users)</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>p50</th>
<th>p95</th>
<th>avg</th>
<th>req/s</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline (poll-based)</td>
<td>1.99ms</td>
<td>6.04ms</td>
<td>2.84ms</td>
<td>1,116/s</td>
</tr>
<tr>
<td>Modded (LISTEN/NOTIFY)</td>
<td>1.65ms</td>
<td>4.80ms</td>
<td>2.13ms</td>
<td>1,131/s</td>
</tr>
</tbody></table>
<p>Here we have a 20% improvement at p95. The trigger-based approach does more work per INSERT (INSERT + NOTIFY), but the reduced round trips and better connection reuse patterns offset the overhead.</p>
<h3 id="heading-the-verdict">The Verdict</h3>
<p><code>LISTEN/NOTIFY</code> works for real-time features where you would otherwise reach for Redis pub/sub. The main limitation is payload size (8,000 bytes maximum) and the requirement for dedicated connections (incompatible with PgBouncer in transaction mode).</p>
<h2 id="heading-the-combined-workload-the-honest-test">The Combined Workload: The Honest Test</h2>
<p>Individual benchmarks are flattering. The real question: can one PostgreSQL instance handle caching, queues, search, and pub/sub simultaneously without degrading?</p>
<h3 id="heading-results-all-four-workloads-running-together">Results (All Four Workloads Running Together)</h3>
<table>
<thead>
<tr>
<th>Mode</th>
<th>p50</th>
<th>p95</th>
<th>avg</th>
<th>req/s</th>
</tr>
</thead>
<tbody><tr>
<td>Baseline</td>
<td>1.65ms</td>
<td>5.24ms</td>
<td>2.17ms</td>
<td>1,424/s</td>
</tr>
<tr>
<td>Modded</td>
<td>1.86ms</td>
<td>6.05ms</td>
<td>2.47ms</td>
<td>1,417/s</td>
</tr>
</tbody></table>
<p>Under combined load, the baseline marginally outperforms the modded setup. The modded PostgreSQL does more work per operation — maintaining GIN indexes, firing triggers, running <code>pg_cron</code> in the background. When all these features are active simultaneously, the overhead is measurable: about 15% higher p95 latency.</p>
<p>But both setups stay comfortably under 10ms at p95. For most web applications, that's more than good enough.</p>
<h2 id="heading-what-i-learned">What I Learned</h2>
<p>After running all these benchmarks, here's what I would tell a team evaluating whether to "just use Postgres":</p>
<ol>
<li><p><strong>Do it for full-text search:</strong> Switching from <code>ILIKE</code> to <code>tsvector</code> with a GIN index is a 10x improvement that requires zero extensions. This is the single highest-ROI change in the entire PostgreSQL ecosystem, and most developers don't know it exists.</p>
</li>
<li><p><strong>Do it for job queues:</strong> <code>SKIP LOCKED</code> is production-ready and eliminates RabbitMQ for simple "process this job" workloads. Use a library like pg-boss or river rather than rolling your own.</p>
</li>
<li><p><strong>Consider it for caching:</strong> UNLOGGED tables give a steady 13% improvement over regular tables. If sub-millisecond latency is not a hard requirement (and for most web apps, it is not), you can drop Redis entirely.</p>
</li>
<li><p><strong>Be honest about the overhead:</strong> Running all four roles simultaneously adds about 15% latency compared to running any single role. Whether that matters depends on your latency budget.</p>
</li>
<li><p><strong>Know where to stop:</strong> PostgreSQL won't match Redis for sub-millisecond caching, Kafka for millions of messages per second, or Elasticsearch for distributed multi-node search with complex analyzers. The line is at extreme throughput or extreme specialization.</p>
</li>
</ol>
<p>The honest conclusion is not "PostgreSQL does everything." It is: for most applications, a single well-configured PostgreSQL instance handles 80% of what you would otherwise need three to five additional services for. That is less infrastructure to deploy, monitor, and maintain — and fewer things to break at 3 AM.</p>
<p>Enterprise-scale applications processing millions of messages per second, serving sub-millisecond cache hits to millions of concurrent users, or running distributed search across terabytes of documents will still need specialized tools. Those tools exist for a reason, and at that scale the operational cost of running them is justified by the performance you get back.</p>
<p>But most of us aren't building at that scale — and may never need to. Starting with PostgreSQL for these roles means you ship faster with fewer moving parts. If and when you outgrow what PostgreSQL can handle, your benchmarks will tell you exactly which role needs to be extracted into a dedicated service. That is a much better position than starting with five services on day one because you assumed you would need them.</p>
<p>The <a href="https://github.com/aaronhsyong2/pg-stack-benchmark">benchmark project</a> is open source if you want to reproduce these results or adapt the tests for your own workload.</p>
<p>You can find more of my writing at <a href="https://site.aaronhsyong.com">site.aaronhsyong.com</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
