<?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: Sauman Arshad</title>
    <description>The latest articles on DEV Community by Sauman Arshad (@saumanarsad).</description>
    <link>https://dev.to/saumanarsad</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1311918%2F1286f5e1-8d66-402b-9e62-2784e65d0060.jpeg</url>
      <title>DEV Community: Sauman Arshad</title>
      <link>https://dev.to/saumanarsad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saumanarsad"/>
    <language>en</language>
    <item>
      <title>When Clean Code Becomes a Performance Nightmare: The Hidden Cost of ORMs</title>
      <dc:creator>Sauman Arshad</dc:creator>
      <pubDate>Thu, 04 Jun 2026 20:33:37 +0000</pubDate>
      <link>https://dev.to/saumanarsad/when-clean-code-becomes-a-performance-nightmare-the-hidden-cost-of-orms-23o0</link>
      <guid>https://dev.to/saumanarsad/when-clean-code-becomes-a-performance-nightmare-the-hidden-cost-of-orms-23o0</guid>
      <description>&lt;p&gt;In modern web development, we crave abstraction. We want our data to feel like a collection of objects, clean, predictable, and type-safe.&lt;/p&gt;

&lt;p&gt;Enter the Object-Relational Mapper (ORM). Tools like TypeORM, Prisma, and Sequelize have saved developers millions of lines of boilerplate SQL. They let us stay in "TypeScript land," treating database tables like simple classes.&lt;/p&gt;

&lt;p&gt;But there is a fundamental conflict at play:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ORMs optimize for &lt;strong&gt;Developer Experience (DX)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Databases optimize for &lt;strong&gt;Execution Plans&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These goals are rarely aligned. If you aren't actively inspecting the SQL your code emits, you're outsourcing performance decisions to an abstraction — and sometimes, that abstraction silently kills performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The N+1 Monster and Connection Exhaustion
&lt;/h2&gt;

&lt;p&gt;The most infamous ORM failure is the N+1 problem. In a local dev environment, it looks harmless. On a production server under load, it's a death spiral.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; You want to list 50 blog posts and show their authors. With a "lazy-loading" ORM, you might write this:&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;// The "Clean" Code that hides the disaster&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;postRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Query 1: Get 50 posts&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// +50 Queries: One for each author&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; by &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this doesn't feel like a crisis. 50 queries at 5ms each is only 250ms. Your local machine handles it fine.&lt;/p&gt;

&lt;p&gt;But at scale — say 200 requests per second (RPS) — the numbers explode:&lt;br&gt;
Total Queries = 200 req/s × 51 queries/req = 10,200 queries/sec&lt;/p&gt;

&lt;p&gt;The result: your database CPU spikes to 100%, and your connection pool is instantly exhausted. New requests begin queuing, waiting for a connection that never comes. Suddenly, your API is down because of a four-line "clean" loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Use relations in find options to fetch everything in a single query:&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;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;postRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;relations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// One query, all data&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The "Select *" Tax
&lt;/h2&gt;

&lt;p&gt;ORMs often default to fetching every column to populate the class definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Your &lt;code&gt;User&lt;/code&gt; table has a 2MB &lt;code&gt;profile_picture&lt;/code&gt; or a heavy JSONB &lt;code&gt;metadata&lt;/code&gt; column. But for a login check, you only need &lt;code&gt;email&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fetching unnecessary data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wastes network I/O&lt;/li&gt;
&lt;li&gt;Prevents the database from using Covering Indexes&lt;/li&gt;
&lt;li&gt;Slows down queries, even if the dataset seems small&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; Use &lt;code&gt;.select()&lt;/code&gt; in QueryBuilder. Move only the data you actually need:&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userRepository&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createQueryBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user.id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user.email&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="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user.id = :id&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="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOne&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Double Writes: The save() vs. insert() Trap
&lt;/h2&gt;

&lt;p&gt;In TypeORM, &lt;code&gt;repository.save(entity)&lt;/code&gt; is convenient because it handles both inserts and updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Catch:&lt;/strong&gt; To decide whether to insert or update, &lt;code&gt;save()&lt;/code&gt; often runs a &lt;code&gt;SELECT&lt;/code&gt; first to check if the record exists. You are literally doubling the database load for every write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; If you know you're creating a new record, use &lt;code&gt;.insert()&lt;/code&gt;. One trip, not two.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The "Database Agnostic" Fallacy
&lt;/h2&gt;

&lt;p&gt;Many developers say: &lt;em&gt;"I use an ORM so I can switch from PostgreSQL to MySQL easily later."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Reality check: &lt;strong&gt;you aren't switching.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By writing "generic" code, you leave massive performance on the table. PostgreSQL has powerful features — JSONB indexes, Lateral Joins, Partial Indexes — that solve hard problems. Ignoring them to stay "generic" is expensive.&lt;/p&gt;

&lt;p&gt;When in doubt, break the abstraction:&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;activeUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    SELECT u.name, p.bio 
    FROM users u 
    INNER JOIN profiles p ON p.user_id = u.id 
    WHERE u.active = true
`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQL in your code isn't dirty. Ignoring your database engine is.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Production Reality: The Cascading Failure
&lt;/h2&gt;

&lt;p&gt;Inefficient ORM-generated SQL doesn't just slow down a single request — it creates contention:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Long-running queries hold locks longer&lt;/li&gt;
&lt;li&gt;Locked rows block updates&lt;/li&gt;
&lt;li&gt;Blocked updates fill the connection pool&lt;/li&gt;
&lt;li&gt;Your entire production environment goes down&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A "simple" entity change can trigger all of this if you're not paying attention.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. The Leaky Abstraction Rule
&lt;/h2&gt;

&lt;p&gt;Abstractions are leaky. Eventually, the underlying complexity — SQL — will seep through and affect your app.&lt;/p&gt;

&lt;p&gt;To operate at a senior level, you must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable Query Logging:&lt;/strong&gt; If one page load generates a wall of SQL, you have a problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop treating databases as "Object Bags":&lt;/strong&gt; They are relational engines, not collections of in-memory objects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break the abstraction when needed:&lt;/strong&gt; A senior dev knows the ORM syntax; a lead dev knows when to ditch it for raw SQL&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The best abstraction is the one you can see through.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>database</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
