<?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: Satyaki Saha</title>
    <description>The latest articles on DEV Community by Satyaki Saha (@satyaki_saha_367d015583da).</description>
    <link>https://dev.to/satyaki_saha_367d015583da</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%2F4138121%2Fde9431ac-5069-4d03-9fe0-7a865198cb2d.png</url>
      <title>DEV Community: Satyaki Saha</title>
      <link>https://dev.to/satyaki_saha_367d015583da</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/satyaki_saha_367d015583da"/>
    <language>en</language>
    <item>
      <title>Just Add More Database Connections Is a System Design Trap</title>
      <dc:creator>Satyaki Saha</dc:creator>
      <pubDate>Sat, 03 Oct 2026 04:18:47 +0000</pubDate>
      <link>https://dev.to/satyaki_saha_367d015583da/just-add-more-database-connections-is-a-system-design-trap-5fnn</link>
      <guid>https://dev.to/satyaki_saha_367d015583da/just-add-more-database-connections-is-a-system-design-trap-5fnn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ub71zkk9ck7if1rsmhh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ub71zkk9ck7if1rsmhh.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;br&gt;
During system design interviews or production firefighting sessions, a common piece of logic always resurfaces: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"If our Hibernate threads are starving for database connections, why don't we just set the connection pool size to 1000? More connections means more parallel queries, which means better database CPU usage... right?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Wrong.&lt;/strong&gt; It sounds intuitive, but in production, this naive scaling approach will actively slow down your system—and under the right circumstances, trigger a total database outage. &lt;/p&gt;

&lt;p&gt;Here is what actually happens when you bloat your connection pool, and the production-grade solutions you should implement instead.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Latency Gets Worse, Not Better (The Myth of Infinite Parallelism)
&lt;/h2&gt;

&lt;p&gt;Imagine a database server running on a machine with &lt;strong&gt;8 CPU cores&lt;/strong&gt;. If you give your application instances hundreds of active connections, all trying to execute heavy queries simultaneously, the database doesn't magically work faster. It thrashes.&lt;/p&gt;

&lt;p&gt;Instead of doing actual processing work, the operating system spends its precious CPU execution cycles doing this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context Switching:&lt;/strong&gt; Rapidly swapping execution context between hundreds of competing database threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock Contention:&lt;/strong&gt; Waiting on row-level or table-level database locks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Thrashing:&lt;/strong&gt; Ruining CPU L1/L2 cache locality as threads fight for resources.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The Downstream Effect
&lt;/h3&gt;

&lt;p&gt;When context switching takes over, &lt;strong&gt;each individual query takes longer to complete&lt;/strong&gt;. Because queries take longer, &lt;strong&gt;connections are held open longer&lt;/strong&gt;. As connections stay occupied, new incoming requests pile up in the application queue. You haven't increased throughput; you have simply turned a high-performance database into a severe traffic jam.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. How Kubernetes HPA Turns a Slowdown Into an Outage
&lt;/h2&gt;

&lt;p&gt;Things get dangerous when you mix giant connection pools with cloud native auto-scaling infrastructure like a Kubernetes &lt;strong&gt;Horizontal Pod Autoscaler (HPA)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine this textbook disaster scenario:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your database starts slowing down due to CPU thrashing.&lt;/li&gt;
&lt;li&gt;Because queries take longer, application response times spike.&lt;/li&gt;
&lt;li&gt;The Kubernetes HPA sees the high latency (or CPU utilization) on your app pods and triggers a scale-out event to save the day—scaling your microservice from &lt;strong&gt;2 pods to 8 pods&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If each pod has a direct connection pool size of &lt;strong&gt;100&lt;/strong&gt;, your deployment suddenly attempts to open &lt;strong&gt;800 client connections&lt;/strong&gt; simultaneously (&lt;code&gt;8 pods × 100 connections&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your Postgres &lt;code&gt;max_connections&lt;/code&gt; configuration limit is capped at &lt;strong&gt;200&lt;/strong&gt;, the autoscaler that was supposed to save your application just delivered the killing blow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FATAL: sorry, too many clients already
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your system shifts instantly from a minor traffic slowdown into a hard production outage.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Production-Grade Solutions
&lt;/h2&gt;

&lt;p&gt;How do you design a system that handles high traffic volumes without starving your microservices or crashing your core engine? &lt;/p&gt;

&lt;h3&gt;
  
  
  A. Introduce a Dedicated Connection Pooler
&lt;/h3&gt;

&lt;p&gt;For databases like PostgreSQL, you should almost &lt;strong&gt;never&lt;/strong&gt; connect highly scalable microservice replicas directly to the core database instance. Implement a proxy tool like &lt;strong&gt;PgBouncer&lt;/strong&gt; or &lt;strong&gt;AWS RDS Proxy&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;A connection pooler acts as a multiplexing buffer. Your 8 Kubernetes pods can safely open 100 client connections each to the proxy (800 total), while the proxy efficiently multiplexes those requests down into a tight, high-performance pool of just &lt;strong&gt;20 to 30 actual physical connections&lt;/strong&gt; directly routing onto the underlying Postgres engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  B. Size the Pool to the DB Hardware, Not the Pod Count
&lt;/h3&gt;

&lt;p&gt;When calculating your absolute baseline connection pool limits, rely on hardware realities over arbitrary scaling numbers. The standard industry benchmark formula established by HikariCP is:&lt;/p&gt;

&lt;p&gt;$$\text{Connections} = (\text{Core Count} \times 2) + \text{Effective Spindles}$$&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: Effective spindles represent the underlying disk I/O capabilities (which varies based on mechanical hard disks vs. solid-state drives).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This formula tells you the sweet spot where the database server can process tasks simultaneously before disk I/O blocking transitions into destructive CPU context thrashing. Take this overall hardware budget and divide it safely across your &lt;em&gt;maximum expected&lt;/em&gt; replica pod count.&lt;/p&gt;

&lt;h3&gt;
  
  
  C. Implement CQRS (Read/Write Splitting)
&lt;/h3&gt;

&lt;p&gt;If your application genuinely demands 1,000 parallel connections to handle aggressive read traffic (e.g., massive reporting dashboards or heavy user profile fetches), &lt;strong&gt;do not route them to your primary transactional write instance&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Separate your connection pools entirely. Route all heavy read operations to a distributed cluster of &lt;strong&gt;Read Replicas&lt;/strong&gt;, keeping your primary database connection pool lean, fast, and strictly dedicated to handling state mutations and writes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ultimate Takeaway
&lt;/h2&gt;

&lt;p&gt;A bigger connection pool is &lt;strong&gt;not&lt;/strong&gt; a faster database. &lt;/p&gt;

&lt;p&gt;A tightly constrained pool, lightning-fast query execution, and a well-managed application-side queue will outperform a bloated, thrashing connection pool every single time. &lt;/p&gt;

&lt;p&gt;Keep your pools small, keep your queries indexed, and use proxy poolers to protect your data layer.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>postgres</category>
      <category>systemdesign</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>We Broke Prod With Cache Misses So You Don't Have To</title>
      <dc:creator>Satyaki Saha</dc:creator>
      <pubDate>Sat, 26 Sep 2026 12:45:40 +0000</pubDate>
      <link>https://dev.to/satyaki_saha_367d015583da/we-broke-prod-with-cache-misses-so-you-dont-have-to-14le</link>
      <guid>https://dev.to/satyaki_saha_367d015583da/we-broke-prod-with-cache-misses-so-you-dont-have-to-14le</guid>
      <description>&lt;p&gt;Today's blog is mostly about the common ways &lt;strong&gt;cache misses&lt;/strong&gt; have happened in our system, and how we resolved them.&lt;/p&gt;

&lt;p&gt;Cache misses — and the system crashes they cause — are something you will face very often when you implement caching in a system. Until you face them, there's no way to know how the system can collapse without them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Key simply doesn't exist yet (cold miss)
&lt;/h3&gt;

&lt;p&gt;First-ever request for that data — nothing wrong here, just an empty cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy:&lt;/strong&gt; Cache-aside (lazy loading) — read from the DB on a miss, populate the cache, then return. This is the default pattern for a reason: it only caches what's actually requested. The upside here is that the request is usually new and not yet a common one, so you can afford not to overthink it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Key exists but fails to deserialize
&lt;/h3&gt;

&lt;p&gt;The bytes are there, but a schema change, version drift, or a bad write left the value unreadable. This one's sneaky — it looks like a miss, but it's actually a read failure being swallowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy:&lt;/strong&gt; Version your cache keys alongside your data schema (e.g., &lt;code&gt;user:v2:123&lt;/code&gt;) so old, incompatible values naturally stop being looked up instead of throwing on read. Also log deserialization failures separately from true misses — conflating them hides real bugs. &lt;em&gt;We faced this issue with a failing Jackson deserialization, which made the system go completely haywire.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. TTL expiry — and the stampede risk on high-value keys
&lt;/h3&gt;

&lt;p&gt;A key ages out normally. That's fine for low-traffic keys. It's dangerous for hot keys: if thousands of concurrent requests hit the same expired key at once, they all miss simultaneously and hammer the DB together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mutex/lock on rebuild — the first request past the miss recomputes the value; others wait or get a stale value.&lt;/li&gt;
&lt;li&gt;Logical expiration — never let the TTL actually delete the key; instead, store an internal &lt;code&gt;expires_at&lt;/code&gt;. On read, if it's expired, serve the stale value immediately and refresh it asynchronously in the background. &lt;em&gt;[This is something we currently implement.]&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Jittered TTLs so hot keys don't all expire in lockstep with each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Request for a resource that doesn't exist (cache penetration)
&lt;/h3&gt;

&lt;p&gt;Someone queries an ID that isn't in the DB either — so nothing ever gets cached, and every repeat of that query bypasses the cache and hits the DB. This can be accidental, or an active exploit (bots probing random IDs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache the negative result too, with a short TTL, so repeated misses for the same nonexistent key stop reaching the DB. &lt;em&gt;This is negative caching, and we currently have this implemented.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Bloom filter in front of the cache/DB to cheaply reject obviously invalid keys before they ever reach the backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Entire cache cluster (server + replicas) went down
&lt;/h3&gt;

&lt;p&gt;This one is severe. Thanks to the holiday-season timing, we were able to resolve it without much of a crisis — but every single request across the system ends up hitting the DB at once. This is a full avalanche, not a localized miss. In our system, we didn't have read sentinels set up properly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High availability for the cache layer itself (Sentinel/Cluster with automatic failover), so one node dying isn't a total outage.&lt;/li&gt;
&lt;li&gt;Circuit breaker / rate limiting in front of the DB as a last line of defense, so a cache outage degrades gracefully instead of taking the DB down too.&lt;/li&gt;
&lt;li&gt;A local in-process L1 cache in front of Redis can cushion the blow even if L2 is fully down.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Network timeout to the cache
&lt;/h3&gt;

&lt;p&gt;The cache is healthy and has the key, but the client times out or the connection drops before it gets the response. Depending on your fail-open/fail-closed policy, this often silently falls through to the DB, even though the data technically wasn't missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>redis</category>
      <category>programming</category>
      <category>backenddevelopment</category>
    </item>
    <item>
      <title>Silent Saves: How JPA Persists Your Changes Without You Asking</title>
      <dc:creator>Satyaki Saha</dc:creator>
      <pubDate>Tue, 22 Sep 2026 18:28:19 +0000</pubDate>
      <link>https://dev.to/satyaki_saha_367d015583da/silent-saves-how-jpa-persists-your-changes-without-you-asking-4iii</link>
      <guid>https://dev.to/satyaki_saha_367d015583da/silent-saves-how-jpa-persists-your-changes-without-you-asking-4iii</guid>
      <description>&lt;p&gt;One of our junior devs wrote an API that was supposed to be &lt;em&gt;&lt;strong&gt;“read-only”&lt;/strong&gt;&lt;/em&gt; and act as an internal data funnel — pull some records, transform them, pass them on. nothing that should ever come close to the database. But it corrupted the production data badly.&lt;/p&gt;

&lt;p&gt;Then I started to dig into the code, and saw that he had @Transactional on the function. He was retrieving entities internally via EntityManager.find() (or a JPQL query, same thing) and &lt;em&gt;mutating some fields on them in the middle of processing&lt;/em&gt;, just as a way to store intermediate state while composing the response. No save, no merge, no repository.update() anywhere in sight, which is exactly what made him feel it was safe.&lt;/p&gt;

&lt;p&gt;But because the entities were managed and the method ran inside an active transaction, every one of those "intermediate state" field mutations got picked up by &lt;em&gt;Hibernate's dirty checking at commit time. What he thought was a harmless, read-only data funnel was silently issuing UPDATE statements against production data&lt;/em&gt; — overwriting real records with transient values that were only ever meant to exist in memory for the duration of the request.&lt;/p&gt;

&lt;p&gt;The fix was intuitive at that point: mark the transaction readOnly = true (@Transactional(readOnly = true)), which tells Hibernate to skip dirty checking and flushing entirely for that context. But 2 things I learnt were Hibernate engineering is a masterclass in abstraction, and learning hibernate is go deep or go home case.&lt;/p&gt;

</description>
      <category>jpa</category>
      <category>hibernate</category>
      <category>programming</category>
      <category>springboot</category>
    </item>
  </channel>
</rss>
