<?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: azhadsuhaimi</title>
    <description>The latest articles on DEV Community by azhadsuhaimi (@azhadsuhaimi).</description>
    <link>https://dev.to/azhadsuhaimi</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%2F4066913%2F96d9bed1-8528-4df9-9ca7-48770c96bc10.png</url>
      <title>DEV Community: azhadsuhaimi</title>
      <link>https://dev.to/azhadsuhaimi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/azhadsuhaimi"/>
    <language>en</language>
    <item>
      <title>"Tech stack doesn't matter" is terrible advice when you're a solo dev</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Tue, 22 Sep 2026 00:58:29 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/tech-stack-doesnt-matter-is-terrible-advice-when-youre-a-solo-dev-4oei</link>
      <guid>https://dev.to/azhadsuhaimi/tech-stack-doesnt-matter-is-terrible-advice-when-youre-a-solo-dev-4oei</guid>
      <description>&lt;p&gt;You’ve probably heard this advice a hundred times on X, Reddit, and Indie Hackers:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Users don't care about your tech stack. Just pick whatever is fast and build your MVP."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In theory, this sounds smart. In practice, as a solo developer, picking a tech stack that you aren't completely comfortable with or that lacks solid production features leads straight to burnout. &lt;/p&gt;

&lt;p&gt;When you spend 80% of your nights debugging OAuth state mismatches, fixing database migration scripts, or fighting CORS policies on deployment, your motivation evaporates. Fast.&lt;/p&gt;

&lt;p&gt;The stack doesn't matter to the user, but it matters immensely to &lt;strong&gt;you&lt;/strong&gt;—the person who has to maintain it at 2 AM.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why I Settled on .NET 8 + Next.js
&lt;/h3&gt;

&lt;p&gt;When I started planning projects at &lt;a href="https://pulselabsmy.com/" rel="noopener noreferrer"&gt;PulseLabs&lt;/a&gt;, I tried various mainstream SaaS templates. Most were heavily biased toward Node.js or Python backends. &lt;/p&gt;

&lt;p&gt;While they're fine for simple apps, I wanted something rock-solid for long-term scalability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strong Typing &amp;amp; Performance:&lt;/strong&gt; .NET 8 brings incredible runtime speed and clean C# syntax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise-Grade Security:&lt;/strong&gt; Built-in identity management and robust ORM features via Entity Framework Core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern Frontend DX:&lt;/strong&gt; Next.js with React Server Components, TypeScript, and Shadcn UI for fast UI development.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem? Finding a complete, production-ready template that seamlessly connects a decoupled .NET Web API to a modern Next.js frontend is surprisingly hard.&lt;/p&gt;

&lt;p&gt;So, I spent the last few months building &lt;strong&gt;NetPulse&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Makes This Setup Different
&lt;/h3&gt;

&lt;p&gt;Instead of forcing a rigid framework, I focused on making the plumbing modular and easy to tear down if needed.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Pluggable Payment Engine
&lt;/h4&gt;

&lt;p&gt;Most templates lock you into Stripe. But if you operate globally, tax compliance is a nightmare. NetPulse includes an abstraction layer that lets you switch between &lt;strong&gt;Stripe&lt;/strong&gt;, &lt;strong&gt;Polar.sh&lt;/strong&gt;, and &lt;strong&gt;Lemon Squeezy&lt;/strong&gt; with a single config change. No core code rewrites required.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Fully Containerized Workflow
&lt;/h4&gt;

&lt;p&gt;Local setup is usually where boilerplates fail. Rather than writing a 20-step setup guide, everything is wrapped inside Docker Compose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js Client&lt;/li&gt;
&lt;li&gt;.NET 8 Web API (Clean Architecture)&lt;/li&gt;
&lt;li&gt;PostgreSQL 16&lt;/li&gt;
&lt;li&gt;Stripe CLI container for routing local webhooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running &lt;code&gt;docker compose up -d --build&lt;/code&gt; gets the entire ecosystem up and running immediately.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Real Clean Architecture
&lt;/h4&gt;

&lt;p&gt;The backend is split into 4 logical layers (Domain, Application, Infrastructure, WebApi). Core business logic is isolated from database drivers and external SDKs, keeping the codebase readable as the app grows.&lt;/p&gt;




&lt;h3&gt;
  
  
  Tech Stack Breakdown
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; .NET 8 Web API (Clean Architecture)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js (App Router), TypeScript, Tailwind CSS, Shadcn UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; PostgreSQL 16 + Entity Framework Core 8&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth System:&lt;/strong&gt; ASP.NET Core Identity, JWT Bearer tokens, Google &amp;amp; GitHub OAuth 2.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe, Polar.sh, Lemon Squeezy (Multi-provider abstraction)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevOps:&lt;/strong&gt; Multi-stage Dockerfiles + Docker Compose&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The Goal
&lt;/h3&gt;

&lt;p&gt;The point of a good boilerplate isn't to over-engineer your app. It's to eliminate all the repetitive setup so you can spend 100% of your energy building the features that actually make your product unique.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>saas</category>
      <category>webdev</category>
      <category>csharp</category>
    </item>
    <item>
      <title>I Spent 3 Weeks Setup-Helling a Full-Stack SaaS Stack Before Writing a Single Line of Business Logic</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Fri, 11 Sep 2026 03:47:26 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/i-spent-3-weeks-setup-helling-a-full-stack-saas-stack-before-writing-a-single-line-of-business-logic-5ah</link>
      <guid>https://dev.to/azhadsuhaimi/i-spent-3-weeks-setup-helling-a-full-stack-saas-stack-before-writing-a-single-line-of-business-logic-5ah</guid>
      <description>&lt;p&gt;Check out &lt;a href="https://pulselabsmy.com/" rel="noopener noreferrer"&gt;PulseLabs&lt;/a&gt; to explore production-ready developer tools, SQL Server performance diagnostics, and full-stack SaaS starters built to help you ship faster!&lt;/p&gt;




&lt;h3&gt;
  
  
  The $5,000 Waste: Why 80% of Indie SaaS Projects Die in Week 2
&lt;/h3&gt;

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

&lt;p&gt;You have a killer SaaS idea. You spend your weekend excited, sketching out database schemas and dreaming of your first 100 paying users. On Monday night, you sit down to start coding.&lt;/p&gt;

&lt;p&gt;By Friday night:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You are down a rabbit hole trying to get ASP.NET Core Identity refresh tokens to play nicely with Next.js App Router client sessions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You are banging your head against the wall because Google and GitHub OAuth redirect callbacks keep returning claim errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your Stripe webhook handler crashes every time a test checkout fires because the payload signature does not match locally.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You have not even written a single line of the actual app feature that your users will pay you for!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is the ultimate momentum killer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most developers do not fail because their idea was bad. They fail because by the time they finish gluing together authentication, database ORMs, billing webhooks, rate limiting, and CORS headers, they are completely burned out.&lt;/p&gt;




&lt;h3&gt;
  
  
  How to Avoid the Boilerplate Burnout Trap
&lt;/h3&gt;

&lt;p&gt;If you want to actually launch your SaaS product before losing your sanity, follow these three rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never build authentication or billing from scratch twice:&lt;/strong&gt; Set up pluggable abstractions for Stripe, Polar, or Lemon Squeezy once, and re-use them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Containerize your local dependencies from Day 1:&lt;/strong&gt; Do not waste days fixing database setup issues or port conflicts on your machine. Use Docker Compose so your DB, API, and frontend boot in a single command.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use standard Clean Architecture patterns:&lt;/strong&gt; Do not mix database logic inside API controllers. Keeping a clear separation between Domain, Application, and Infrastructure layers prevents spaghetti code as your app grows.  &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Meet NetPulse: The Production-Ready .NET 8 + Next.js SaaS Starter
&lt;/h3&gt;

&lt;p&gt;To stop wasting weeks on repetitive setup work, I built NetPulse—an enterprise-grade SaaS starter kit engineered for developers who want to bypass the setup headache and start shipping features immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Breakdown
&lt;/h3&gt;

&lt;p&gt;NetPulse follows strict Clean Architecture principles on the backend combined with a modern Next.js 14 App Router client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NetPulse/
├── client/                               # Next.js App Router Frontend
│   ├── src/
│   │   ├── app/                          # Auth pages, Dashboard &amp;amp; Landing
│   │   ├── components/                   # UI components &amp;amp; Radix wrappers
│   │   └── lib/                          # Axios API clients &amp;amp; helpers
│   └── Dockerfile
├── src/                                  # .NET 8 Backend Solution
│   ├── Core/
│   │   ├── Domain/                       # Enterprise Entities &amp;amp; Domain models
│   │   ├── Application/                  # Business logic, DTOs &amp;amp; CQRS
│   │   └── Infrastructure/               # EF Core, Identity, Payment Gateways
│   └── WebApi/                           # Controllers &amp;amp; Middlewares
├── .env.example
└── docker-compose.yml                    # Multi-container setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Is Included Under the Hood?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dual Gateway Billing Engine:&lt;/strong&gt; Built-in multi-provider support for &lt;strong&gt;Stripe&lt;/strong&gt; and &lt;strong&gt;Polar.sh&lt;/strong&gt; with automated webhook handlers for subscription lifecycles (&lt;code&gt;subscription.created&lt;/code&gt;, &lt;code&gt;checkout.created&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise Authentication:&lt;/strong&gt; ASP.NET Core Identity with &lt;strong&gt;JWT Bearer Access Tokens&lt;/strong&gt;, refresh tokens, and social login for &lt;strong&gt;Google &amp;amp; GitHub&lt;/strong&gt; out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-configured Security:&lt;/strong&gt; Middleware for IP Rate Limiting, Audit Logging, and Global Exception Handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turnkey Docker Setup:&lt;/strong&gt; Launch PostgreSQL, .NET 8 Web API, and Next.js in 1 command.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Spinning Up in Under 5 Minutes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone repository and copy environment variables&lt;/span&gt;
git clone &amp;lt;your-repo&amp;gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;NetPulse
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env

&lt;span class="c"&gt;# Spin up containers&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;

&lt;span class="c"&gt;# Run database migrations&lt;/span&gt;
dotnet ef database update &lt;span class="nt"&gt;--project&lt;/span&gt; src/Core/Infrastructure &lt;span class="nt"&gt;--startup-project&lt;/span&gt; src/WebApi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it! Your entire backend, database, client app, and API documentation (Swagger) are up and running.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Do not let setup fatigue ruin another great project idea. Focus your time on solving real user problems and building unique product features—not wrestling with identity protocols or billing webhooks.&lt;/p&gt;

&lt;p&gt;Explore more developer tools, diagnostic scripts, and production starters over at &lt;a href="https://pulselabsmy.com/" rel="noopener noreferrer"&gt;PulseLabs&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>docker</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I was tired of spending 3 weeks setting up Auth &amp; Billing, so I built NetPulse</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Mon, 07 Sep 2026 08:28:55 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/i-was-tired-of-spending-3-weeks-setting-up-auth-billing-so-i-built-netpulse-42fn</link>
      <guid>https://dev.to/azhadsuhaimi/i-was-tired-of-spending-3-weeks-setting-up-auth-billing-so-i-built-netpulse-42fn</guid>
      <description>&lt;p&gt;Let’s be honest for a second. &lt;/p&gt;

&lt;p&gt;Every time a cool SaaS idea pops up in your head at 2 AM, the excitement is unreal. You fire up your terminal, create a blank project, and get ready to build something awesome.&lt;/p&gt;

&lt;p&gt;Then reality hits.&lt;/p&gt;

&lt;p&gt;Before you can write a single line of your actual core feature, you spend the next three weeks setting up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth flow that doesn't break on callbacks&lt;/li&gt;
&lt;li&gt;JWT access and refresh token logic&lt;/li&gt;
&lt;li&gt;Entity Framework models &amp;amp; Postgres migrations&lt;/li&gt;
&lt;li&gt;Stripe CLI and local webhook forwarding&lt;/li&gt;
&lt;li&gt;Docker compose files because "it works on my machine" isn't enough anymore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time you finish configuring all that boring plumbing, the initial high is gone. The project gets abandoned, joining the graveyard of unfulfilled GitHub repos.&lt;/p&gt;

&lt;p&gt;I’ve been there way too many times while working on projects at &lt;a href="https://pulselabsmy.com/" rel="noopener noreferrer"&gt;PulseLabs&lt;/a&gt;. So over the last couple of months, I decided to fix this loop once and for all.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;NetPulse&lt;/strong&gt;—a .NET 8 + Next.js SaaS starter kit designed to get all that setup out of the way in about 5 minutes.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Pain Points I Wanted to Eliminate
&lt;/h3&gt;

&lt;p&gt;I didn't want just another bloated template. I wanted something clean, fast, and sensible for developers who actually want to ship.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Payment Provider Lock-In
&lt;/h4&gt;

&lt;p&gt;Most templates force you to use Stripe. But depending on where your users are or how you handle international taxes, you might prefer a Merchant of Record (MoR) like &lt;strong&gt;Polar.sh&lt;/strong&gt; or &lt;strong&gt;Lemon Squeezy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding one gateway, I wrapped the billing system inside a simple abstraction layer. You get pre-built webhook handlers for Stripe, Polar, and Lemon Squeezy out of the box. Want to switch providers? Just change your config in &lt;code&gt;Program.cs&lt;/code&gt; or &lt;code&gt;.env&lt;/code&gt;. You don't have to rewrite your core subscription logic.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Clean Architecture (Without the Over-Engineering)
&lt;/h4&gt;

&lt;p&gt;I like clean code, but I hate over-engineering. The backend uses a straightforward 4-layer setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain:&lt;/strong&gt; Pure business entities, zero external dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application:&lt;/strong&gt; CQRS handlers, DTOs, interfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; EF Core, PostgreSQL, mailers, payment SDKs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebApi:&lt;/strong&gt; Controllers, rate limiting, audit logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ever decide to swap out PostgreSQL for something else, your domain logic stays completely safe.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. One Command Local Setup
&lt;/h4&gt;

&lt;p&gt;No one likes installing five different dependencies on their local machine just to test a template. &lt;/p&gt;

&lt;p&gt;Everything runs inside Docker. One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spins up the Next.js frontend, the .NET Web API, a PostgreSQL 16 container, and a Stripe CLI container that automatically routes local webhooks to your local API.&lt;/p&gt;

&lt;h4&gt;
  
  
  What’s Under the Hood?
&lt;/h4&gt;

&lt;p&gt;Here’s the TL;DR of the tech stack:&lt;/p&gt;

&lt;p&gt;Backend: .NET 8 Web API (Clean Architecture)&lt;/p&gt;

&lt;p&gt;Frontend: Next.js (App Router), TypeScript, Tailwind CSS, Shadcn UI&lt;/p&gt;

&lt;p&gt;Database: PostgreSQL 16 + Entity Framework Core 8&lt;/p&gt;

&lt;p&gt;Auth: ASP.NET Core Identity, JWT Bearer, Google &amp;amp; GitHub OAuth 2.0&lt;/p&gt;

&lt;p&gt;Payments: Pluggable integration for Stripe, Polar.sh, and Lemon Squeezy&lt;/p&gt;

&lt;p&gt;DevOps: Multi-stage Dockerfiles + Docker Compose&lt;/p&gt;

&lt;h4&gt;
  
  
  Final Thoughts
&lt;/h4&gt;

&lt;p&gt;Building this was all about cutting down friction. By baking the repetitive 80% of SaaS setup into a reusable foundation, the focus shifts entirely to building the actual product features and shipping faster.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>nextjs</category>
      <category>saas</category>
    </item>
    <item>
      <title>Why I Built a .NET 8 + Next.js SaaS Boilerplate (And 4 Architecture Decisions I Made)</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Tue, 01 Sep 2026 10:44:55 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/why-i-built-a-net-8-nextjs-saas-boilerplate-and-4-architecture-decisions-i-made-1h0h</link>
      <guid>https://dev.to/azhadsuhaimi/why-i-built-a-net-8-nextjs-saas-boilerplate-and-4-architecture-decisions-i-made-1h0h</guid>
      <description>&lt;p&gt;Building a full-stack SaaS application from scratch is usually fun—until you reach the part where you have to wire up identity managers, ORM migrations, CORS policies, OAuth redirects, and payment webhooks.&lt;/p&gt;

&lt;p&gt;Over the past two months of building at &lt;a href="https://pulselabsmy.com/" rel="noopener noreferrer"&gt;PulseLabs&lt;/a&gt;, I found myself solving the exact same foundation problems repeatedly. To eliminate this friction permanently, I engineered &lt;strong&gt;NetPulse&lt;/strong&gt;: a production-ready starter kit leveraging .NET 8 Web API, Next.js App Router, PostgreSQL 16, and Docker.&lt;/p&gt;

&lt;p&gt;Instead of a generic feature walkthrough, I want to share the &lt;strong&gt;4 core architectural decisions&lt;/strong&gt; I made during this build and why I structured it this way.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Enforcing Clean Architecture in the Backend
&lt;/h3&gt;

&lt;p&gt;Many SaaS templates bundle all backend logic directly inside API controllers or Next.js route handlers. While fast initially, it quickly turns into spaghetti code as the business logic grows.&lt;/p&gt;

&lt;p&gt;For NetPulse, I separated the backend into 4 distinct projects within a single solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NetPulse.sln
├── Core/
│   ├── Domain/         # Pure enterprise entities (User, Subscription, AuditLog)
│   ├── Application/    # DTOs, Service Interfaces, CQRS &amp;amp; Business Logic
│   └── Infrastructure/ # EF Core, Identity, Mailers, Payment SDKs
└── WebApi/             # Controllers, Middlewares, Program.cs setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this matters:&lt;br&gt;
The Domain layer has zero dependencies on external libraries. If you want to switch from Entity Framework Core to Dapper, or swap database providers, your core business logic remains completely untouched.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Abstraction Layer for Payment Billing (Stripe, Polar, Lemon Squeezy)
One of the most frustrating decisions when launching a project is choosing a payment provider. Traditional processors like Stripe are great, but managing international VAT and sales tax can be a nightmare for solo builders. Merchant of Record (MoR) solutions like Polar.sh or Lemon Squeezy handle taxes automatically.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of hardcoding a single vendor, I built a pluggable abstraction layer around an IBillingService interface.&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IBillingService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CheckoutSessionResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateCheckoutSessionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;priceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HandleWebhookAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;jsonPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;signatureHeader&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SyncSubscriptionStatusAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;customerId&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;How it works in practice:&lt;br&gt;
Developers using the template don't need to rebuild webhook infrastructure when changing providers. You simply set your preferred active provider in Program.cs / .env (e.g., Stripe, Polar, or Lemon Squeezy), and the core identity and subscription models automatically handle tier updates (Free, Pro, Enterprise).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;JWT Bearer Tokens + ASP.NET Core Identity &amp;amp; OAuth
Handling authentication between a decoupled SPA (Next.js) and a backend API (.NET) can get tricky with CORS and cookie/token security.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I implemented ASP.NET Core Identity with custom JWT Bearer handling, paired with OAuth 2.0 social sign-ins (Google &amp;amp; GitHub).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Next.js Client ] ──(OAuth Redirect)──► [ Provider (Google/GitHub) ]
         │                                          │
         │ (Receive Callback Code)                   │
         ▼                                          ▼
[ Send Code to .NET API ] ───────────────► [ Validate &amp;amp; Issue JWT + Refresh Token ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Short-lived access tokens keep stateless API calls secure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refresh tokens stored in PostgreSQL allow seamless session renewal without forcing the user to re-login.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Zero-Lock-In Containerization via Docker Compose
Setting up PostgreSQL, .NET SDK, Node modules, and local webhook listeners manually on every new development machine takes hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I containerized the ecosystem using a single root docker-compose.yml. With one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It spins up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Next.js Frontend (localhost:3000)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;.NET 8 Web API (localhost:44366 with interactive Swagger UI)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PostgreSQL 16 Container (localhost:5432)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stripe CLI Container (automatically tunneling live local webhooks to the backend controller)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🛠️ Summary of Tech Stack&lt;br&gt;
⚡ Backend API: ASP.NET Core 8 Web API structured with Clean Architecture (Domain, Application, Infrastructure, WebApi)&lt;/p&gt;

&lt;p&gt;🎨 Frontend Client: Next.js (App Router), TypeScript, Tailwind CSS, and Shadcn UI / Radix primitives&lt;/p&gt;

&lt;p&gt;🗄️ Database &amp;amp; ORM: PostgreSQL 16 + Entity Framework Core 8 (Npgsql) with code-first migrations&lt;/p&gt;

&lt;p&gt;💳 Pluggable Billing: Pre-configured handlers for Stripe, Polar.sh, and Lemon Squeezy&lt;/p&gt;

&lt;p&gt;🔐 Auth &amp;amp; Security: ASP.NET Core Identity, JWT Bearer tokens, and OAuth 2.0 (Google &amp;amp; GitHub)&lt;/p&gt;

&lt;p&gt;🐳 DevOps &amp;amp; Tooling: Multi-stage Dockerfiles orchestrated via a single Docker Compose setup&lt;/p&gt;

&lt;p&gt;Let's Discuss Architecture 🗣️&lt;br&gt;
If you're building SaaS products with .NET or Next.js, I'd love to hear your thoughts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Clean Architecture vs. Vertical Slice: Do you prefer splitting code into strict layers (Domain/App/Infra), or grouping everything by feature folder?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database Choice: Do you lean towards PostgreSQL or SQL Server when deploying .NET Web APIs in production?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payment Gateways: Has anyone here transitioned from Stripe to an MoR provider like Polar or Lemon Squeezy? How was your experience?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Leave a comment below—let's chat code! 👇&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>nextjs</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building NetPulse: A Production-Ready .NET 8 + Next.js SaaS Boilerplate</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Fri, 28 Aug 2026 16:34:26 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/building-netpulse-a-production-ready-net-8-nextjs-saas-boilerplate-273e</link>
      <guid>https://dev.to/azhadsuhaimi/building-netpulse-a-production-ready-net-8-nextjs-saas-boilerplate-273e</guid>
      <description>&lt;p&gt;Over the last 1–2 months, I stopped chasing new side-project ideas and decided to solve a frustration I’ve had for a long time: &lt;strong&gt;The exhausting setup phase before writing actual business logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As part of my ongoing engineering work at &lt;a href="https://pulselabsmy.com/" rel="noopener noreferrer"&gt;PulseLabs&lt;/a&gt;, I frequently test and prototype product ideas. But every single time, I found myself spending 3–4 weeks repeating the exact same setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuring OAuth callback handling&lt;/li&gt;
&lt;li&gt;Designing Entity Framework models and running PostgreSQL migrations&lt;/li&gt;
&lt;li&gt;Handling JWT access &amp;amp; refresh tokens securely&lt;/li&gt;
&lt;li&gt;Wiring up payment gateways and handling brittle webhooks&lt;/li&gt;
&lt;li&gt;Building Docker environments that actually work locally and in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the time the app foundation was ready, the initial momentum was gone. &lt;/p&gt;

&lt;p&gt;So, I spent the last two months building &lt;strong&gt;NetPulse&lt;/strong&gt;—an enterprise-grade, clean-architecture SaaS boilerplate designed to crush that setup phase down to under 5 minutes.&lt;/p&gt;

&lt;p&gt;Here is what I focused on solving during this build, how I structured the project, and the architectural decisions I made along the way.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Solving "Multi-Provider Payment Lock-In"
&lt;/h3&gt;

&lt;p&gt;Most SaaS templates lock you into a single payment gateway (usually just Stripe). But depending on where your customers are or how you handle international taxes/MoR (Merchant of Record), you might prefer &lt;strong&gt;Polar.sh&lt;/strong&gt; or &lt;strong&gt;Lemon Squeezy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding Stripe everywhere, I engineered a &lt;strong&gt;Multi-Gateway Billing Engine&lt;/strong&gt; in the backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               ┌──────────────────────┐
               │  IBillingService     │ (Abstraction Layer)
               └──────────┬───────────┘
                          │
     ┌────────────────────┼────────────────────┐
     ▼                    ▼                    ▼
[ StripeProvider ]  [ PolarProvider ]  [ LemonSqueezyProvider ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether a subscription event (Free, Pro, Enterprise) comes from Stripe, Polar, or Lemon Squeezy, the API standardizes subscription lifecycles, checkout sessions, and webhook processing seamless inside ASP.NET Core.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Tech Stack: Clean Architecture + Modern Frontend
I wanted a stack that offers blazing performance, type safety, and zero bloat:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Backend: ASP.NET Core (.NET 8 Web API) using Clean Architecture (Domain, Application, Infrastructure, and WebApi presentation layers).&lt;/p&gt;

&lt;p&gt;Database &amp;amp; ORM: PostgreSQL 16 + Entity Framework Core 8 (Npgsql) with code-first migrations and connection pooling.&lt;/p&gt;

&lt;p&gt;Frontend: Next.js (App Router, React 18, TypeScript, Tailwind CSS, Shadcn UI / Radix primitives).&lt;/p&gt;

&lt;p&gt;Auth &amp;amp; Security: ASP.NET Core Identity + JWT Bearer tokens + Google &amp;amp; GitHub OAuth 2.0. Integrated with Audit Logging, Rate Limiting, and Global Exception middleware.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The "One-Command Setup" Nightmare (Solved with Docker)
Setting up a full-stack environment with .NET, Node, PostgreSQL, and local webhook tunnels manually is a headache for developers cloning a starter kit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I packaged the entire ecosystem into a single docker-compose.yml setup. Running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boots up the Next.js frontend, .NET 8 Web API, PostgreSQL 16 DB, and an automated Stripe CLI webhook tunnel container simultaneously.&lt;/p&gt;

&lt;p&gt;Here’s a quick glance at the repository structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NetPulse/
├── client/          # Next.js App Router Frontend
├── src/             # .NET 8 Backend Solution
│   ├── Core/        # Domain &amp;amp; Application Business Logic
│   └── WebApi/      # Controllers, Middlewares &amp;amp; Config
├── docker-compose.yml
└── NetPulse.sln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key Lessons Learned from 2 Months of Building&lt;br&gt;
Separation of Concerns saves sanity: Keeping payment provider integrations inside the Infrastructure layer means swapping gateways doesn't touch the core business logic.&lt;/p&gt;

&lt;p&gt;Developer Experience (DX) matters most: A boilerplate shouldn't just be "code"—it needs step-by-step documentation, working .env.example templates, and zero-headache local deployment.&lt;/p&gt;

&lt;p&gt;I’d Love Your Feedback! 💬&lt;br&gt;
As I finalize version 1.0 of NetPulse, I’d love to get insights from fellow .NET devs, Next.js builders, and SaaS founders:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Payment Gateways: Do you prefer using traditional processors like Stripe, or Merchant of Record (MoR) platforms like Polar.sh / Lemon Squeezy for your SaaS?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Backend Architecture: Do you prefer strict Clean Architecture (4 layers) for SaaS templates, or a Vertical Slice Architecture approach?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is the single most annoying feature you hate setting up manually in every new project?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop your thoughts in the comments below! I'll be active in the thread replying to architecture questions. 👇&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>nextjs</category>
      <category>saas</category>
    </item>
    <item>
      <title>I Spent 30+ Hours Building "The Boring SaaS Stack" So I Never Have To Do It Again</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Tue, 18 Aug 2026 02:13:05 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/i-spent-30-hours-building-the-boring-saas-stack-so-i-never-have-to-do-it-again-pa9</link>
      <guid>https://dev.to/azhadsuhaimi/i-spent-30-hours-building-the-boring-saas-stack-so-i-never-have-to-do-it-again-pa9</guid>
      <description>&lt;p&gt;We’ve all been there: &lt;/p&gt;

&lt;p&gt;You get an exciting idea for a micro-SaaS on a Friday night. You fire up your IDE, ready to build the core feature that solves a real problem. &lt;/p&gt;

&lt;p&gt;Fast forward 3 weeks later... and you haven't even written a single line of business logic yet. &lt;/p&gt;

&lt;p&gt;Instead, you’ve spent your precious free time configuring Google OAuth, wrestling with Stripe webhook signatures, building user profile pages, and tweaking Dark Mode CSS variables for the 100th time.&lt;/p&gt;

&lt;p&gt;By the time the "boring 80%" of the app is finally functional, the initial excitement is gone, burnout hits, and another side project dies silently in a local directory.&lt;/p&gt;




&lt;h3&gt;
  
  
  Taming the "Boring 80%"
&lt;/h3&gt;

&lt;p&gt;I got tired of repeating this painful cycle every time I wanted to test a new product idea. So, I decided to freeze new feature builds for a moment and focus entirely on creating a clean, unbloated foundation.&lt;/p&gt;

&lt;p&gt;My goal was simple: &lt;strong&gt;Build a lean, production-ready setup once, and reuse it forever.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the lightweight stack and core architecture I put together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; ASP.NET Core Web API (Clean, predictable, and fast execution)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Layer:&lt;/strong&gt; Social Sign-In (Google &amp;amp; GitHub OAuth) to eliminate email/password management overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing:&lt;/strong&gt; Stripe integration handling subscription checkouts (Pro / Enterprise tiers) and webhook listener setups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI/UX:&lt;/strong&gt; Responsive Dashboard, basic credential profile management, and native Light/Dark theme toggling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a quick high-level look at how I kept the user context and session management minimal:&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="c1"&gt;// Keeping auth payload lean while preserving claim context&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurrentUserContext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;SubscriptionTier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="s"&gt;"Free"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsActive&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SubscriptionTier&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"Free"&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;The Big Dilemma: How Much "Boilerplate" Is Too Much?&lt;br&gt;
While structuring this setup, I noticed a huge trap in existing commercial boilerplates: Over-engineering.&lt;/p&gt;

&lt;p&gt;Many templates try to throw in every single library under the sun—multi-tenancy, complex RBAC permissions, 5 different payment gateways, blog engines, and heavy microservice abstractions. You end up spending more time deleting code you don't need than actually building your app.&lt;/p&gt;

&lt;p&gt;I wanted to keep mine as stripped-down as possible while still covering the actual launch requirements.&lt;/p&gt;

&lt;p&gt;I'd Love Your Input! 💬&lt;br&gt;
Before I finalize version 1.0 of this foundation for my upcoming micro-SaaS launches, I want to hear from fellow builders and backend devs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Multi-Tenancy vs. Single-User: Is built-in Organization/Team management an absolute MUST-HAVE for v1, or is simple single-user auth enough for an initial MVP?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payment Integrations: Do you prefer sticking exclusively to Stripe, or is LemonSqueezy/Paddle support a dealbreaker for global taxes?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is the single most annoying thing you usually hate about 3rd-party starter kits/boilerplates?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop your thoughts, hot takes, or architectural advice in the comments below! 👇&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>The Silent Killer of ASP.NET Core Performance: The N+1 Query Problem</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Thu, 13 Aug 2026 03:26:10 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/the-silent-killer-of-aspnet-core-performance-the-n1-query-problem-1akc</link>
      <guid>https://dev.to/azhadsuhaimi/the-silent-killer-of-aspnet-core-performance-the-n1-query-problem-1akc</guid>
      <description>&lt;p&gt;It’s a classic story: &lt;/p&gt;

&lt;p&gt;You build an API endpoint in ASP.NET Core using Entity Framework Core. You test it locally with 10 dummy records in your database, and it responds in a lightning-fast &lt;strong&gt;15 milliseconds&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;You push it to production. A few weeks later, as the user base and data grow, users start complaining that the app is loading painfully slow. You check your APM logs, and to your horror, a single HTTP GET request is triggering &lt;strong&gt;1,001 SQL queries&lt;/strong&gt; to the database.&lt;/p&gt;

&lt;p&gt;Welcome to the &lt;strong&gt;N+1 Query Problem&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Actually Happens Under the Hood?
&lt;/h3&gt;

&lt;p&gt;The N+1 problem occurs when your application executes &lt;strong&gt;1 query&lt;/strong&gt; to fetch a parent record (or list of records), and then executes &lt;strong&gt;N additional queries&lt;/strong&gt; to fetch related child data for every single parent item in that list.&lt;/p&gt;

&lt;p&gt;Consider this innocent-looking LINQ code:&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="c1"&gt;// 1 Query to fetch 100 active customers&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customers&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;Customers&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;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&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;span class="k"&gt;foreach&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;customer&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// N Queries executed inside the loop!&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;latestOrder&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;FirstOrDefaultAsync&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;customer&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="c1"&gt;// Process order...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have 100 customers:&lt;/p&gt;

&lt;p&gt;1 Query fetches the list of customers.&lt;/p&gt;

&lt;p&gt;100 Queries are fired inside the foreach loop to get each customer's order.&lt;/p&gt;

&lt;p&gt;Total database roundtrips = 101. Multiply that by network latency, and your database connection pool starts crying for help.&lt;/p&gt;

&lt;p&gt;How to Catch It Before It Hits Production&lt;br&gt;
EF Core makes lazy loading or lazy queries inside loops deceptively easy. Here are three ways to hunt down N+1 queries in your codebase:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable SQL Logging in Development
In your appsettings.Development.json, set EF Core logging to Information:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Microsoft.EntityFrameworkCore.Database.Command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Watch your terminal output. If you see a wall of identical SELECT statements scrolling past for a single request, you've got an N+1 issue.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Throw Exceptions on Unintended Queries
If you use Lazy Loading, you can explicitly configure EF Core to throw an exception in development whenever a query is triggered implicitly:
&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="n"&gt;optionsBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureWarnings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warnings&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
        &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Throw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RelationalEventId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MultipleCollectionIncludeWarning&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The Fix: Eager Loading &amp;amp; Projection&lt;br&gt;
Instead of fetching data inside loops, tell EF Core exactly what related data you need up-front so it generates a single JOIN, or project directly into a DTO.&lt;/p&gt;

&lt;p&gt;Approach A: Eager Loading (Include)&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="c1"&gt;// Generates a SINGLE SQL JOIN query&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customersWithOrders&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;Customers&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;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&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="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;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&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;Approach B: Direct Projection (Best Performance)&lt;br&gt;
Only fetch the fields your API actually returns. This avoids fetching unneeded columns and forces EF Core to construct an optimized SQL query:&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;customerDtos&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;Customers&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;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&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;c&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="n"&gt;CustomerDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CustomerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&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;CustomerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LatestOrderDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&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;Max&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderDate&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;Conclusion&lt;br&gt;
N+1 queries rarely show up as errors—they just quietly degrade your performance as your database scales. By adopting direct DTO projection and monitoring SQL logs during local development, you can catch these bottlenecks before your production database takes a hit.&lt;/p&gt;

&lt;p&gt;How do you usually catch hidden ORM query issues in your team? Do you rely on APM tools, EF Core logging, or static analysis tools?&lt;/p&gt;

&lt;p&gt;Let me know in the comments below!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>webdev</category>
      <category>database</category>
    </item>
    <item>
      <title>Why Is My SQL Query Slow Only in Production? (The Parameter Sniffing Trap)</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Mon, 10 Aug 2026 01:08:43 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/why-is-my-sql-query-slow-only-in-production-the-parameter-sniffing-trap-1ki9</link>
      <guid>https://dev.to/azhadsuhaimi/why-is-my-sql-query-slow-only-in-production-the-parameter-sniffing-trap-1ki9</guid>
      <description>&lt;p&gt;It’s every developer's favorite Friday afternoon nightmare:&lt;/p&gt;

&lt;p&gt;A user complains that a feature in the app is hanging. You take the exact SQL query executed by the application, paste it into SQL Server Management Studio (SSMS), hit Execute... and it finishes in &lt;strong&gt;0.02 seconds&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;You run it again. Blazing fast. &lt;/p&gt;

&lt;p&gt;Yet, inside the application, it continues to time out. &lt;/p&gt;

&lt;p&gt;If you’ve been building database-backed apps long enough, you’ve almost certainly run into &lt;strong&gt;Parameter Sniffing&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Actually Happens Under the Hood?
&lt;/h3&gt;

&lt;p&gt;When a parameterized query or Stored Procedure runs for the very first time, SQL Server looks at the parameters passed in at that specific moment. It uses those values to estimate how many rows will be returned and compiles an execution plan tailored for that payload.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scenario A:&lt;/strong&gt; The first run passes a parameter that returns 5 rows. SQL Server creates a plan using an &lt;strong&gt;Index Seek&lt;/strong&gt;. Fast and lightweight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenario B:&lt;/strong&gt; Later, another user passes a parameter that returns 500,000 rows. SQL Server reuses the cached "Index Seek" plan instead of doing an &lt;strong&gt;Index Scan&lt;/strong&gt;. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result? The server chokes trying to force a lightweight plan onto a massive dataset.&lt;/p&gt;




&lt;h3&gt;
  
  
  How to Catch Bad Plans in the Cache
&lt;/h3&gt;

&lt;p&gt;Instead of guessing or restarting the SQL Server service (which wipes the entire cache and hides the evidence!), you can inspect the plan cache to see what parameter values were used during compilation versus execution.&lt;/p&gt;

&lt;p&gt;Here’s a quick DMV snippet I use to find queries where the average execution duration is wildly higher than expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Exec_Count&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Avg_Duration_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Avg_CPU_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_logical_reads&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Avg_Logical_Reads&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;
          &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Query_Text&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_plan&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;XML_Execution_Plan&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_stats&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_sql_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Clicking&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;XML_Execution_Plan&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="n"&gt;lets&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="k"&gt;view&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="n"&gt;graphical&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="n"&gt;directly&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SSMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the Parameter List inside the properties window—it will show you the Compiled Value vs the Runtime Value. If they look drastically different, you've found your parameter sniffing culprit.&lt;/p&gt;

&lt;p&gt;How Do You Usually Fix Parameter Sniffing?&lt;br&gt;
There are several ways to tackle this depending on the SQL Server version and business context:&lt;/p&gt;

&lt;p&gt;Adding OPTIMIZE FOR UNKNOWN to the query.&lt;/p&gt;

&lt;p&gt;Using local variables inside stored procedures.&lt;/p&gt;

&lt;p&gt;Updating stale index statistics.&lt;/p&gt;

&lt;p&gt;Using Query Store (if enabled) to force a known good plan.&lt;/p&gt;

&lt;p&gt;How do you usually handle this in your production apps? Do you rely on query hints, or do you prefer fixing it at the database configuration level?&lt;/p&gt;

&lt;p&gt;Drop your thoughts in the comments!&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why Do We Keep Forgetting About the Plan Cache in SQL Server?</title>
      <dc:creator>azhadsuhaimi</dc:creator>
      <pubDate>Fri, 07 Aug 2026 23:26:04 +0000</pubDate>
      <link>https://dev.to/azhadsuhaimi/why-do-we-keep-forgetting-about-the-plan-cache-in-sql-server-3l7e</link>
      <guid>https://dev.to/azhadsuhaimi/why-do-we-keep-forgetting-about-the-plan-cache-in-sql-server-3l7e</guid>
      <description>&lt;p&gt;Picture this: A user reports that the system is running painfully slow. &lt;/p&gt;

&lt;p&gt;What is your immediate knee-jerk reaction? &lt;/p&gt;

&lt;p&gt;For a long time, mine was to check table indexes, look at active locks, or blame the ORM for generating terrible SQL queries. But more often than not, the actual root cause was sitting right under my nose—inside SQL Server’s &lt;strong&gt;plan cache&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We talk a lot about optimizing queries during development, but we rarely talk about how much insight SQL Server passively collects for us in production while it's running.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Unsung Hero: &lt;code&gt;sys.dm_exec_query_stats&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When SQL Server executes a query, it compiles an execution plan and caches it to save CPU time on subsequent runs. Along with that plan, it tracks execution metrics inside &lt;code&gt;sys.dm_exec_query_stats&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;It stores critical telemetry like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Execution Count:&lt;/strong&gt; Is a query slow because it’s inherently heavy, or because it’s being executed 50,000 times a minute (the classic N+1 problem)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker Time:&lt;/strong&gt; How much raw CPU time (&lt;code&gt;total_worker_time&lt;/code&gt;) has this specific query consumed since the last service restart?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logical Reads:&lt;/strong&gt; Is the query thrashing memory and IO?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of firing up SQL Server Profiler or Extended Events (which can add performance overhead to a live server), pulling directly from the plan cache is practically free.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Tricky Part: Taming Large Batch Queries
&lt;/h3&gt;

&lt;p&gt;If you've ever queried &lt;code&gt;sys.dm_exec_query_stats&lt;/code&gt; and joined it with &lt;code&gt;sys.dm_exec_sql_text&lt;/code&gt;, you’ve probably hit a common pain point: &lt;strong&gt;It gives you the entire batch or stored procedure text.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a slow statement is hidden inside a 500-line stored procedure, looking at the entire text block doesn't immediately tell you &lt;em&gt;which exact query&lt;/em&gt; ate the CPU.&lt;/p&gt;

&lt;p&gt;To isolate the specific culprit, you have to do some offset math using &lt;code&gt;statement_start_offset&lt;/code&gt; and &lt;code&gt;statement_end_offset&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a lightweight snippet I usually keep handy when I need to quickly inspect top CPU hogs without reading through endless blocks of SQL text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Execution_Count&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total_CPU_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_count&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Avg_CPU_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_elapsed_time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total_Duration_ms&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_logical_reads&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Total_Logical_Reads&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;creation_time&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Plan_Cached_Since&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;DB_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dbid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Database_Name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;OBJECT_NAME&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objectid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dbid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Object_Name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="k"&gt;SUBSTRING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;
              &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;DATALENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_end_offset&lt;/span&gt;
          &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statement_start_offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Query_Text&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_stats&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_sql_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="n"&gt;APPLY&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dm_exec_query_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plan_handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qp&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_worker_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dividing total_worker_time by 1000 converts microseconds to readable milliseconds, and the SUBSTRING logic slices out the exact offending query statement.&lt;/p&gt;

&lt;p&gt;What's Your First Move During a Performance Issue?&lt;br&gt;
I’ve been working on organizing my own lightweight, read-only diagnostic scripts lately because I got tired of rewriting these DMV queries from memory during high-pressure troubleshooting.&lt;/p&gt;

&lt;p&gt;But I’m curious to know how other developers handle this:&lt;/p&gt;

&lt;p&gt;Do you regularly rely on Dynamic Management Views (DMVs) for quick health checks?&lt;/p&gt;

&lt;p&gt;Or do you prefer APM tools like Datadog, New Relic, or built-in Extended Events?&lt;/p&gt;

&lt;p&gt;Let’s discuss in the comments below!&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
