<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mugendi Njue</title>
    <description>The latest articles on DEV Community by Mugendi Njue (@mugendinjue3).</description>
    <link>https://dev.to/mugendinjue3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F424941%2Ff250b0ea-1afd-47df-9cee-f3b88972e1a4.jpg</url>
      <title>DEV Community: Mugendi Njue</title>
      <link>https://dev.to/mugendinjue3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mugendinjue3"/>
    <language>en</language>
    <item>
      <title>Postgres Logical Replication for Reporting Replicas: The Gotchas Nobody Puts in the Tutorial</title>
      <dc:creator>Mugendi Njue</dc:creator>
      <pubDate>Thu, 24 Sep 2026 20:24:17 +0000</pubDate>
      <link>https://dev.to/mugendinjue3/postgres-logical-replication-for-reporting-replicas-the-gotchas-nobody-puts-in-the-tutorial-5nj</link>
      <guid>https://dev.to/mugendinjue3/postgres-logical-replication-for-reporting-replicas-the-gotchas-nobody-puts-in-the-tutorial-5nj</guid>
      <description>&lt;p&gt;If you've read the Postgres docs on logical replication, you've seen the happy path: &lt;code&gt;CREATE PUBLICATION&lt;/code&gt;, &lt;code&gt;CREATE SUBSCRIPTION&lt;/code&gt;, wait a minute, query your replica. It looks so clean that you start to wonder why anyone would bother with anything else. I fell for that cleanliness too. Then I put it in front of a real reporting workload, and it bit me in four distinct, very avoidable ways. This is the field report I wish someone had handed me first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Logical Replication for a Reporting Replica (and Not Physical)
&lt;/h2&gt;

&lt;p&gt;Physical replication ships raw WAL — the byte-for-byte changes to disk blocks. It's simple and rock solid, but it's all-or-nothing: you replicate the &lt;em&gt;entire&lt;/em&gt; cluster, same major version, same everything. Great for failover. Terrible for a reporting replica where you want to add extra indexes for analytical queries, run a different Postgres minor version, or replicate only three tables out of two hundred because analysts don't need your &lt;code&gt;audit_log&lt;/code&gt; table clogging up disk.&lt;/p&gt;

&lt;p&gt;Logical replication decodes the WAL into an actual logical representation — "row X in table Y got this UPDATE" — and replays those as SQL-like operations on the subscriber. That decoupling is the whole appeal: selective tables, extra indexes on the replica, even a different schema layout downstream. It reads simple. It is not simple once you're the one operating it. That's the tradeoff, and it's the reason every gotcha below exists in the first place — you gave up physical fidelity for logical flexibility, and flexibility always has an operational cost hiding in it somewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #1: DDL Isn't Replicated — Schema Drift Sneaks Up on You
&lt;/h2&gt;

&lt;p&gt;Here's the one that got me first, and it's honestly the most important thing to internalize about how logical decoding works. Logical replication decodes &lt;em&gt;row changes&lt;/em&gt; out of the WAL — inserts, updates, deletes. It does not decode schema changes. &lt;code&gt;ALTER TABLE ADD COLUMN&lt;/code&gt;, &lt;code&gt;CREATE INDEX&lt;/code&gt;, &lt;code&gt;DROP TABLE&lt;/code&gt; — none of that crosses the wire. Ever.&lt;/p&gt;

&lt;p&gt;So what happens when someone on your team adds a column to &lt;code&gt;orders&lt;/code&gt; in production? Nothing breaks immediately, which is exactly the trap. The publisher writes rows with the new column. The subscriber's version of &lt;code&gt;orders&lt;/code&gt; doesn't have it, and replication just quietly starts erroring on the next &lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;INSERT&lt;/code&gt; touching that column, or worse — silently drops the new field if you didn't add it as &lt;code&gt;NOT NULL&lt;/code&gt;. Depending on your Postgres version, an unrelated column addition might not error at all until someone runs a query expecting the new field on the replica and gets nothing.&lt;/p&gt;

&lt;p&gt;The fix isn't a Postgres feature — it's discipline. Any DDL change on the publisher needs to be applied to the subscriber in the same maintenance step, before or in lockstep with the app deploy that starts writing the new shape. I now treat schema migrations as two-phase: migrate publisher, migrate subscriber, &lt;em&gt;then&lt;/em&gt; deploy code. Skipping that middle step is how schema drift sneaks in, and it always sneaks in through the "small, harmless" migration nobody thought to coordinate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: Replication Slots That Never Advance and Silently Eat Your Disk
&lt;/h2&gt;

&lt;p&gt;This one is scarier because it eats disk, not rows. A logical replication slot on the publisher is a promise: "I will retain WAL until this subscriber confirms it consumed it." If the subscriber disconnects — network blip, replica restart, someone drops the subscription without dropping the slot — the publisher keeps every WAL segment since the last confirmed position. Forever. Or at least until your disk fills up and your primary database, the one actually serving production traffic, grinds to a halt.&lt;/p&gt;

&lt;p&gt;This is the sharpest edge in the whole system because the failure mode is delayed and invisible until it's catastrophic. A slot sitting idle for three days doesn't show up as an error — it shows up as &lt;code&gt;pg_wal&lt;/code&gt; slowly climbing. Watch &lt;code&gt;pg_replication_slots.active&lt;/code&gt; and &lt;code&gt;restart_lsn&lt;/code&gt; lag in bytes, not just "is it connected." I now alert on WAL retained-by-slot size crossing a threshold, not on subscriber connectivity, because connectivity alone tells you nothing about the disk bomb quietly ticking on the publisher.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #3: Sequences, Large Objects, and Other Things Pub/Sub Quietly Skips
&lt;/h2&gt;

&lt;p&gt;Logical replication only replicates table data through publications you explicitly define — and even within that, it has blind spots by design. Sequences are the classic one: if your reporting replica is meant to be a faithful copy for read queries that use &lt;code&gt;nextval()&lt;/code&gt; logic downstream, surprise — sequence values aren't replicated at all. Your replica's sequence state drifts from the publisher's the moment either side calls &lt;code&gt;nextval&lt;/code&gt;. Large objects (&lt;code&gt;pg_largeobject&lt;/code&gt;) aren't replicated either; logical decoding just doesn't touch them. Same story for &lt;code&gt;TRUNCATE&lt;/code&gt; unless you explicitly enable it per-publication, and for unlogged tables, which never had WAL to decode from in the first place.&lt;/p&gt;

&lt;p&gt;None of this is a bug. It's the tradeoff for decoding at the logical row level instead of the physical block level. But it means "logical replication" quietly does not mean "a complete copy of the database," and treating it that way is how you get a reporting replica that's subtly wrong on exactly the kinds of derived state — IDs, blobs — that someone eventually builds a dashboard on top of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #4: Initial Sync and Table Locking on Large Production Tables
&lt;/h2&gt;

&lt;p&gt;When a subscription first comes up, Postgres has to get the subscriber to a consistent starting point before it can start applying streamed changes. That means a full table copy of every table in the publication, and that copy takes a &lt;code&gt;ACCESS SHARE&lt;/code&gt; lock plus, more painfully, blocks on anything holding stronger locks — and on genuinely large tables, that initial &lt;code&gt;COPY&lt;/code&gt; can run for hours. I hit this standing up a reporting replica against a 200GB events table: the sync didn't fail, it just sat there, and meanwhile the publisher's connection pool had one more long-running snapshot to account for.&lt;/p&gt;

&lt;p&gt;The practical mitigation is boring but works: bring subscriptions up during low-traffic windows, or stage the initial copy per-table if your workload allows disabling &lt;code&gt;copy_data&lt;/code&gt; on the subscription and backfilling manually with a controlled &lt;code&gt;COPY&lt;/code&gt; you can pause. It's more manual work up front for less risk of contention later. As always, no free lunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Checklist and Lessons
&lt;/h2&gt;

&lt;p&gt;If I were starting this over, here's what I'd wire up before day one instead of after the first incident: WAL retained per replication slot (not just connection state), a scheduled diff of publisher/subscriber schema, a job to periodically resync sequence values, and an alert on initial-sync duration for any new subscription. Logical replication is a genuinely elegant piece of engineering — decoding a transaction log into row-level intent is a neat trick, and I still enjoy watching it work. But "elegant" and "hands-off" are not the same thing. Every bit of flexibility it gives you over physical replication is flexibility you now have to operate yourself.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>replication</category>
      <category>backend</category>
    </item>
    <item>
      <title>Postgres Logical Replication for Reporting Replicas: The Gotchas the Tutorials Skip</title>
      <dc:creator>Mugendi Njue</dc:creator>
      <pubDate>Thu, 24 Sep 2026 20:05:23 +0000</pubDate>
      <link>https://dev.to/mugendinjue3/postgres-logical-replication-for-reporting-replicas-the-gotchas-the-tutorials-skip-3725</link>
      <guid>https://dev.to/mugendinjue3/postgres-logical-replication-for-reporting-replicas-the-gotchas-the-tutorials-skip-3725</guid>
      <description>&lt;p&gt;If you've read a Postgres logical replication tutorial, you know the drill. &lt;code&gt;CREATE PUBLICATION&lt;/code&gt; on the primary, &lt;code&gt;CREATE SUBSCRIPTION&lt;/code&gt; on the replica, wait a few seconds, and boom — your data is flowing. It looks so simple that you start to wonder why anyone bothers with the more complicated physical replication setups.&lt;/p&gt;

&lt;p&gt;I fell for that simplicity too. Then I built a real reporting replica — the kind analysts hammer with ad-hoc queries all day, running a different Postgres major version than the source, with its own indexes tuned for reads instead of writes — and I hit four gotchas that nobody mentions in the getting-started guide. Let me walk you through them, because they will bite you if you don't plan for them upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Logical Replication (Not Physical Standbys) for Reporting Workloads
&lt;/h2&gt;

&lt;p&gt;First, why logical at all? A physical standby replicates the entire cluster byte-for-byte at the WAL block level. It's fast and battle-tested, but it's all-or-nothing — you get every database, every table, and the replica has to run the exact same major version as the primary. You can't add an extra reporting index without also adding it on the primary. You can't run heavy analytical queries without them showing up as replay lag that can starve your standby.&lt;/p&gt;

&lt;p&gt;Logical replication works differently. Instead of shipping raw data pages, Postgres decodes the WAL into logical changes — "row X was updated to this value" — and replays those as actual SQL-like operations on the subscriber. That decoupling is the whole point: you can replicate just the tables reporting actually needs, add reporting-only indexes on the replica, and even run a newer Postgres version there. It's a fundamentally different tool for a fundamentally different job — not a faster physical replica, a &lt;em&gt;selective&lt;/em&gt; one.&lt;/p&gt;

&lt;p&gt;That flexibility is not free, though. You're now decoding and replaying, not just streaming bytes, and you've traded the simplicity of "the replica is the primary" for a handful of edges that require your attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #1: DDL Isn't Replicated — Schema Drift Sneaks Up on You
&lt;/h2&gt;

&lt;p&gt;Here's the first surprise: logical replication only replicates DML — inserts, updates, deletes. It does not replicate DDL. Add a column on the primary, and your subscription doesn't know it exists. Depending on the replica identity and column defaults, you'll either get silent gaps in reporting data or the replication worker will just stop with an error about a missing column, and nobody notices until a dashboard is empty.&lt;/p&gt;

&lt;p&gt;The fix isn't automatic — it's discipline. Every schema migration touching a published table needs a corresponding, deliberately ordered step on the subscriber, applied &lt;em&gt;before&lt;/em&gt; or &lt;em&gt;in lockstep with&lt;/em&gt; the primary's migration, depending on direction of change. Some teams script this into their migration tooling explicitly rather than trusting it to happen by accident. Treat DDL against replicated tables as a two-database transaction, even though Postgres will never enforce that for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: Sequences Don't Sync — Your Serial Columns Lag Behind
&lt;/h2&gt;

&lt;p&gt;This one is sneaky because it doesn't fail loudly. Sequences backing your &lt;code&gt;SERIAL&lt;/code&gt;/&lt;code&gt;IDENTITY&lt;/code&gt; columns are not replicated by logical replication at all. The row data flows through fine — id 4821, id 4822 — but the sequence object itself on the subscriber sits wherever it started.&lt;/p&gt;

&lt;p&gt;Why does this matter for a &lt;em&gt;read-only&lt;/em&gt; reporting replica? It usually doesn't, until someone decides to write to the replica for a one-off fix, or you promote that replica during a failover and suddenly &lt;code&gt;nextval()&lt;/code&gt; hands back an id that already exists. I've seen this cause duplicate key errors weeks after a "read-only" replica quietly became load-bearing for something else. If there's any chance your reporting replica could ever take writes, sync sequence values manually as part of your setup and any resync process — Postgres won't do it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #3: Initial Sync and Large Tables — The COPY Phase Can Lock Up Your Source
&lt;/h2&gt;

&lt;p&gt;When you first attach a subscription, Postgres has to get the subscriber caught up to a consistent starting point. It does this with an initial data copy — a &lt;code&gt;COPY&lt;/code&gt; of the entire table — before it starts applying incremental changes. For a small table, this is instant and forgettable. For a 400GB fact table, it's a long-running transaction on the source holding a snapshot, competing for I/O, and potentially extending vacuum-related bloat while it runs.&lt;/p&gt;

&lt;p&gt;If you're doing this against a live production primary during business hours, you can absolutely cause visible slowdowns — the kind that get you paged. The pragmatic move is to schedule initial syncs for your largest tables during low-traffic windows, publish tables incrementally rather than the whole schema at once, and monitor replication lag and source-side load during that copy phase, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #4: Orphaned Replication Slots Silently Eating Your WAL Disk Space
&lt;/h2&gt;

&lt;p&gt;This is the one that will actually take your primary down. A replication slot is Postgres's promise to the subscriber: "I will not discard WAL you haven't consumed yet." That's exactly what makes logical replication reliable — but it's also a landmine. If a subscriber disconnects, gets decommissioned, or you tear down a reporting replica without dropping its slot, Postgres keeps that promise forever. WAL just accumulates on the primary, disk fills up, and eventually writes fail cluster-wide.&lt;/p&gt;

&lt;p&gt;I've seen this happen from something as mundane as a test subscriber spun up once and forgotten. The lesson: monitor &lt;code&gt;pg_replication_slots&lt;/code&gt; for inactive slots and their &lt;code&gt;restart_lsn&lt;/code&gt; distance from current WAL, and make dropping the slot an explicit, mandatory step whenever you decommission a subscriber. Don't let "the replica is gone" and "the slot is gone" be two separate facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Pragmatic Checklist for Running Logical Replication Reporting Replicas in Production
&lt;/h2&gt;

&lt;p&gt;None of this makes logical replication a bad choice — it's genuinely the right tool for selective, cross-version, read-optimized reporting replicas. But go in with eyes open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version-control DDL changes to published tables and apply them to subscribers in lockstep, never after the fact.&lt;/li&gt;
&lt;li&gt;Sync sequence values explicitly after initial load and after any resync — don't assume they matched, because they didn't.&lt;/li&gt;
&lt;li&gt;Schedule initial syncs for large tables during low-traffic windows and consider publishing tables in batches instead of all at once.&lt;/li&gt;
&lt;li&gt;Alert on inactive replication slots and their WAL retention distance — treat an orphaned slot as a production incident waiting to happen, not a cleanup task.&lt;/li&gt;
&lt;li&gt;Monitor replication lag continuously, not just at setup time — a reporting replica that's silently hours behind is worse than one that's clearly broken.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tutorials show you the two commands that make replication start. What they don't show you is everything Postgres is quietly &lt;em&gt;not&lt;/em&gt; doing for you in between. Know that list, and logical replication is a genuinely excellent tool. Ignore it, and it's a slow-motion incident with your name on it.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>replication</category>
      <category>backend</category>
    </item>
    <item>
      <title>First Class Functions</title>
      <dc:creator>Mugendi Njue</dc:creator>
      <pubDate>Sun, 13 Dec 2020 22:42:16 +0000</pubDate>
      <link>https://dev.to/mugendinjue3/first-class-functions-k7h</link>
      <guid>https://dev.to/mugendinjue3/first-class-functions-k7h</guid>
      <description>&lt;p&gt;Today i learned that JavaScript is a lightweight, interpreted,or JIT(Just In Time) compiled language with first class functions.&lt;br&gt;
What are &lt;strong&gt;first class functions&lt;/strong&gt;? Well a programming language is said to have first class functions when functions in that programming language are treated like any other variable.Example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Assigning a variable to a function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FooBar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Invoking the function&lt;/span&gt;
&lt;span class="nx"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also have a function that returns another function, for example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sayHello&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Method 1 of function invoking&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myFunc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sayHello&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;myFunc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Method 2 of function invoking&lt;/span&gt;
&lt;span class="nx"&gt;sayHello&lt;/span&gt;&lt;span class="p"&gt;()();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functions that return other functions are called &lt;strong&gt;Higher-Order Functions.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>100daysofcode</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
