<?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: Soledayo Fadakinte</title>
    <description>The latest articles on DEV Community by Soledayo Fadakinte (@techytro).</description>
    <link>https://dev.to/techytro</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%2F4035303%2F77f3667d-dfbb-43be-9fa5-9f55997e08c6.png</url>
      <title>DEV Community: Soledayo Fadakinte</title>
      <link>https://dev.to/techytro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/techytro"/>
    <language>en</language>
    <item>
      <title>From 2 Minutes to Sub-200ms: How a Single Implicit ORM Query Time-Outed Our Production API</title>
      <dc:creator>Soledayo Fadakinte</dc:creator>
      <pubDate>Sat, 18 Jul 2026 13:08:49 +0000</pubDate>
      <link>https://dev.to/techytro/from-2-minutes-to-sub-200ms-how-a-single-implicit-orm-query-time-outed-our-production-api-579b</link>
      <guid>https://dev.to/techytro/from-2-minutes-to-sub-200ms-how-a-single-implicit-orm-query-time-outed-our-production-api-579b</guid>
      <description>&lt;p&gt;It started with a warning ping from our production monitoring bot:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Slow API Response Detected&lt;br&gt;
GET /api/vendors/store/reviews&lt;br&gt;
Duration: 38,201 ms&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Within hours, that latency crept up to &lt;strong&gt;66 seconds&lt;/strong&gt;, then &lt;strong&gt;126 seconds&lt;/strong&gt;, before finally collapsing into a string of HTTP 500 server timeouts.&lt;/p&gt;

&lt;p&gt;The feature itself was incredibly simple: load a paginated list of reviews for a store, showing the user’s comment, their star rating, and a tiny order summary.&lt;/p&gt;

&lt;p&gt;So, how did a simple review card end up taking over two minutes to load?&lt;/p&gt;

&lt;p&gt;The answer lies in a common trap that developers fall into when using ORMs like Entity Framework: &lt;strong&gt;unintentional query creep&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Culprit: The "Simple" Query That Wasn't&lt;/strong&gt;&lt;br&gt;
When we dug into the service layer, the query looked something like this under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;reviews&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerReviews&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On paper, this looks normal. We are grabbing the reviews, including the user who wrote it, and including the order they placed.&lt;/p&gt;

&lt;p&gt;But here is where it went off the rails. The review card displayed a summary of the order. To build this summary, the code called a shared mapping class: OrderMapping.OrderResource().&lt;br&gt;
This mapping was a massive, 230-line projection that was originally designed for the &lt;strong&gt;checkout detail screen&lt;/strong&gt;. It joined almost every table in our schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order items&lt;/li&gt;
&lt;li&gt;Product option groups and individual options&lt;/li&gt;
&lt;li&gt;Recipient delivery addresses&lt;/li&gt;
&lt;li&gt;Delivery ranges and pricing calculations&lt;/li&gt;
&lt;li&gt;Rider information, vehicle details, and active statuses&lt;/li&gt;
&lt;li&gt;Tracking timelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the ORM was instructed to load these relationships, it executed a second, massive round-trip database query for every single order attached to every review on the page.&lt;/p&gt;

&lt;p&gt;In production, under load, this created a massive SQL statement with over 10 table joins executing repeatedly. As our review count grew, the database simply choked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix: Slimming down the payload&lt;/strong&gt;&lt;br&gt;
The solution wasn't to write complex SQL, but to apply clean query principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ditch the Generic Projections&lt;/strong&gt;
Instead of reusing the heavy checkout mapping, we created a slim, inline projection that only selected the 5 fields the review card actually needed:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderSummaries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&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="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orderIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderItems&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="n"&gt;oi&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&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="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;By selecting only the required columns, we eliminated the expensive joins on addresses, riders, and options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Run Aggregate Queries in Parallel&lt;/strong&gt;&lt;br&gt;
The endpoint was also running a heavy GroupBy on the reviews table to calculate positive vs. negative counts. We split these into two fast scalar queries and ran them in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;positiveTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rating&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;negativeTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CountAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rating&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positiveTask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;negativeTask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Add Cache with a Short TTL&lt;/strong&gt;&lt;br&gt;
Once the database query was fast, we added a short 2-minute Redis cache key using the pagination and filter parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cacheKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"vendor_reviews:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;vendorId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:page:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pageNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ReviewResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4 Takeaways for Junior &amp;amp; Mid-Level Devs&lt;/strong&gt;&lt;br&gt;
If you are building APIs that need to scale, keep these rules in mind:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Always Inspect Your Generated SQL&lt;/strong&gt;&lt;br&gt;
Don't trust your ORM blindly. Use diagnostic tools or query logging in your development environment to see the actual SQL hitting the database. If you see dozens of joins for a simple card, something is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Project Directly to DTOs&lt;/strong&gt;&lt;br&gt;
Avoid returning database entities directly to your controller. Project your database queries directly into Data Transfer Objects (DTOs) using .Select(). This ensures you only pull the columns you intend to send back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Caching is not a band-aid&lt;/strong&gt;&lt;br&gt;
Never cache a slow, broken query to "fix" performance. Optimize the query first so a cache-miss is still fast, then layer on caching to handle high traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Small choices compound&lt;/strong&gt;&lt;br&gt;
An extra join or two feels harmless on a clean local machine with a database of 10 rows. In production, with millions of rows and hundreds of concurrent users, those small choices scale into server crashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the most memorable performance bug you've ever had to hunt down in production? Let me know in the comments below!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>database</category>
      <category>dotnet</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
