<?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: Prithwiraj Das</title>
    <description>The latest articles on DEV Community by Prithwiraj Das (@prithwiraj_das_8bc02a0871).</description>
    <link>https://dev.to/prithwiraj_das_8bc02a0871</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%2F3838757%2Fc159d4c1-3090-4788-99aa-dc0caa6c64c5.jpg</url>
      <title>DEV Community: Prithwiraj Das</title>
      <link>https://dev.to/prithwiraj_das_8bc02a0871</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prithwiraj_das_8bc02a0871"/>
    <language>en</language>
    <item>
      <title>The Bugs Were Never in the New Code</title>
      <dc:creator>Prithwiraj Das</dc:creator>
      <pubDate>Sun, 28 Jun 2026 05:00:21 +0000</pubDate>
      <link>https://dev.to/prithwiraj_das_8bc02a0871/the-bugs-were-never-in-the-new-code-19md</link>
      <guid>https://dev.to/prithwiraj_das_8bc02a0871/the-bugs-were-never-in-the-new-code-19md</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7xhr9edh3uz47a9ursy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7xhr9edh3uz47a9ursy.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;br&gt;
When you add a feature to an old system, the new code is usually fine. The bugs come from the old code's assumptions, the ones nobody wrote down because back then they were just true.&lt;/p&gt;

&lt;p&gt;I hit a small version of this recently. One feature, three bugs, all the same underlying thing. It wasn't until afterward that I realized I'd been running into this same pattern for years and had never written any of it down. The examples here are from a C# stack, but the shape is the same across the stacks I've worked in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;A multi-tenant platform. One screen had a long-standing way of doing something, gated behind some legacy config. We were adding a newer way to do the same thing, opt-in, for a subset of customers. So for a transition period there were two flows living side by side on the same screen.&lt;/p&gt;

&lt;p&gt;This is a familiar shape if you've modernized anything live. You don't get to delete the old flow on day one. The old and the new have to coexist, and that coexistence is where the assumptions quietly pile up.&lt;/p&gt;

&lt;p&gt;It shipped, and for most customers it worked. Then one slice of customers, the ones who had the new flow turned on and none of the old config, hit a blank screen on freshly created records. A refresh fixed it.&lt;/p&gt;

&lt;p&gt;That last detail is the tell. When a refresh fixes it, the data is usually fine and the rendering path is where things went wrong. When I pulled the thread, it wasn't one bug. It was three, stacked on top of each other, and each one turned out to be the same mistake wearing different clothes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug one: a sibling pretending to be a child
&lt;/h2&gt;

&lt;p&gt;The new logic had been added inside the conditional that handled the old flow.&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="n"&gt;ScreenData&lt;/span&gt; &lt;span class="nf"&gt;GetScreenData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;recordId&lt;/span&gt;&lt;span class="p"&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;legacyData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_legacyRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordId&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;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ScreenData&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;legacyData&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="c1"&gt;// gate for the OLD flow&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Legacy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Populate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;legacyData&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;_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasNewFeature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;// NEW logic, nested inside&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFeature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;PopulateNewFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordId&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&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;Customers who had both flows were fine, because the legacy data existed for them. Customers who had only the new flow failed the outer check, the whole block was skipped, and the new logic nested inside it never ran.&lt;/p&gt;

&lt;p&gt;Here is the part I want to be honest about. I read this code. It did not announce itself. Sitting next to the code it lived beside, nesting it was the natural-looking thing to do, and nothing on the page says it's wrong. The only way to see the problem is to already know there's a customer config where that outer condition is false, and that fact isn't written anywhere in the code. The fix is to make the new logic a sibling instead of a child:&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="n"&gt;ScreenData&lt;/span&gt; &lt;span class="nf"&gt;GetScreenData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;recordId&lt;/span&gt;&lt;span class="p"&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;legacyData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_legacyRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordId&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;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ScreenData&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;legacyData&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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Legacy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Populate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;legacyData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;FetchContextDirectly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// new-only path still works&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;_customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasNewFeature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                  &lt;span class="c1"&gt;// SIBLING: runs in both cases&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFeature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;PopulateNewFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;);&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;result&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;h2&gt;
  
  
  Bug two: a heuristic that expired
&lt;/h2&gt;

&lt;p&gt;The screen refreshed its content only when one specific field changed, because historically that field was the only thing the panel depended on.&lt;/p&gt;

&lt;p&gt;Finding this one took a minute, because everything upstream looked correct. The API returned the right data. The debugger showed the right model. The values were all there, and the panel still sat empty until a manual refresh. That narrowed it to one place: something had stopped telling the panel it needed to redraw.&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;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnSave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;changedFields&lt;/span&gt;&lt;span class="p"&gt;)&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;changedFields&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;PrimaryField&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;     &lt;span class="c1"&gt;// the one thing it used to depend on&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;RefreshPanel&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new feature introduced a second thing the panel now depended on, and the refresh logic knew nothing about it. So in the exact case where the new dependency changed but the original field didn't, the panel never refreshed. The check was correct the day it was written. It stopped being correct the moment the data started depending on something else.&lt;/p&gt;

&lt;p&gt;I didn't re-plumb this one. It was a small feature already moving through the release pipeline, and rewriting the refresh path the right way wasn't something I could fit into that window. So it got a targeted fix and a note for later. That's the real version of this work: the right fix and the shippable fix aren't always the same one, and the feature in the pipeline doesn't wait for the cleanup you'd rather do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug three: context that fell off a boundary
&lt;/h2&gt;

&lt;p&gt;This is the one that's actually interesting.&lt;/p&gt;

&lt;p&gt;The old framework had a built-in way to embed one piece of rendered output inside another. The new framework dropped it, so somewhere along the line it had been rebuilt by hand to keep the old views working. The rebuild rendered the embedded piece by spinning up a fresh, isolated sub-request.&lt;/p&gt;

&lt;p&gt;In a multi-tenant system, "isolated" is the whole problem. The tenant context, the thing that tells the system whose data it's allowed to read, lived in the ambient state of the outer request. The sub-request didn't inherit it.&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;// Outer request: tenant is present in ambient scope&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;recordId&lt;/span&gt;&lt;span class="p"&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;tenant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ambient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tenant&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// "customer 42"&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// correctly scoped to customer 42&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;BuildModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// controller has the RIGHT data&lt;/span&gt;

    &lt;span class="c1"&gt;// ...the hand-rolled embed helper re-renders a fragment:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;EmbedFragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;EmbedFragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_requestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// fresh scope, NO ambient tenant&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// scoped to nothing -&amp;gt; empty&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fragment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// empty overwrites the correct data&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;View&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&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;So the right data was fetched, then thrown away and re-fetched in a context that no longer knew the tenant. The re-fetch came back empty and overwrote the good result with nothing. Blank screen.&lt;/p&gt;

&lt;p&gt;The deeper thing underneath it: ambient context doesn't cross a boundary. The moment you open a sub-request, a thread, a background job, or a queue message, whatever was "just there" stops being there. That's the piece that came back on me later at a much bigger scale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnd8kj8cioq7ao8dkweip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnd8kj8cioq7ao8dkweip.png" alt="A request flowing along a track carries a " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One mistake, three forms
&lt;/h2&gt;

&lt;p&gt;It was only after the third one that I saw all three were the same shape. None of them were failures in the new feature. Each one came from something the old system had quietly depended on for years and never stated.&lt;/p&gt;

&lt;p&gt;A branch nested too deep, a trigger scoped too narrow, a rebuild that lost its context. Underneath, the same thing: an assumption that was true in the old system and false in the new one, never written down because it never needed to be.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapevwrnted36a56m5gbo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapevwrnted36a56m5gbo.png" alt="Three labeled bugs, " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI did and didn't do
&lt;/h2&gt;

&lt;p&gt;I planned this feature with AI in the loop, so it's worth being straight about where it helped and where it didn't.&lt;/p&gt;

&lt;p&gt;My history with this work has three stages. For years I did it by reading code for days and holding the system in my head, and I still missed things, because nobody holds a whole system at once. Then AI became a faster lookup and a decent rubber duck. Now it's more than that: I hand it a scoped piece of discovery, get an analysis back, turn that into a plan, take the plan to the people who need to sign off on it, and execute. It doesn't replace the judgment. It shortens the distance from a question to a plan I can stand behind.&lt;/p&gt;

&lt;p&gt;That's how this one went. AI helped me map the existing flow and draft the change. I wrote the transition plan and walked the team through it. The plan was sound, and we shipped it. It still missed all three seams.&lt;/p&gt;

&lt;p&gt;The reason isn't "AI is a junior and I'm the senior who catches its mistakes." AI is good at the code that's there. None of these bugs were in the code that's there. They were in things written down nowhere: which customer configs exist, how people actually move through the screen, where the tenant context silently rides along. You can't read those in the new code, because they aren't in it.&lt;/p&gt;

&lt;p&gt;There's a scoping piece too, and it has nothing to do with how smart the model is. When you hand a model a task, you hand it a boundary, and it works inside that boundary. The blast radius of the change doesn't respect the boundary. A change scoped to "the new feature's logic" came out in the rendering path, the refresh trigger, and the tenant plumbing, three places outside the frame I'd drawn. That's not the model failing the task. That's the scope not matching the system. And I draw the scope too narrow for the same reason it does: the parts that break are the parts nobody marked as connected. The blind spot was shared, and it came from two directions at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same three mistakes, at backend scale
&lt;/h2&gt;

&lt;p&gt;None of this is specific to web frameworks. It's what modernization does at any scale, and it gets more expensive the deeper down it goes. The same three mistakes show up when you break a monolith into services, and they show up in an order most write-ups skip.&lt;/p&gt;

&lt;p&gt;Nobody starts at the database. You start at the easy edges. Logging goes first, because it's nearly standalone and pulling it out costs almost nothing. The database comes last, because the database is where these migrations stall. Logic is cheap to move; data isn't. So the monolith database keeps running as the source of truth for everything that hasn't moved, each new service comes up with its own store, and a warehouse on the side stays in step through change-data-capture over an event hub or Kafka.&lt;/p&gt;

&lt;p&gt;The textbook says you finish by decommissioning the monolith and its database. In practice that step usually never comes. The old database stays underneath as load-bearing infrastructure nobody is funded to pull out. It's too entangled to be worth removing, so you leave it, the same way you leave a bug that's out of scope for the feature that's already shipping. The pattern is supposed to replace the monolith. Most of the time it just learns to live with it.&lt;/p&gt;

&lt;p&gt;Every one of those moves is one of the three bugs, grown up. Routing capabilities around the monolith instead of through it is the same sibling-or-child question: does this run beside the old path, or inside it. The event-fed warehouse is the expired heuristic: it stays correct exactly as long as every real change emits an event, and the first write that slips through without one leaves the read store quietly wrong, with no error to announce it. And the cross-service context is the lost-boundary problem, which is the worst of the three. In the monolith, tenant and trace and transaction are all just present, because everything shares one scope. Add a service hop and none of it travels unless you carry it on purpose.&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;// Monolith: context is ambient, just there&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// tenant, trace, transaction all implicitly available&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Distributed: the boundary strips it unless you pack it in explicitly&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderEvent&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// forgot to attach TenantId / TraceId to the message...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;Consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderEvent&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&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;tenant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// null — whose data is this? nobody knows.&lt;/span&gt;
    &lt;span class="c1"&gt;// ...processing happens against the wrong (or no) tenant scope&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My screen bug was a tenant ID that didn't survive a sub-request inside a single process. The same bug across services is a tenant ID that doesn't make it onto a published message, and it surfaces in a consumer far away that's now processing data without knowing whose it is. Same mistake, much bigger radius. It's the whole reason distributed tracing and explicit context propagation exist as a discipline: ambient context doesn't cross boundaries on its own.&lt;/p&gt;

&lt;p&gt;There's a smaller one in the same family worth naming, because it bites early. When you finally do move logic out of a stored procedure and into application code, people expect the database load to drop. It usually climbs. The procedure did one round trip with a join:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- one round trip, one result set&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&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="o"&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;customer_id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;     &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&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="k"&gt;WHERE&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="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "modernized" version moves the join into code and quietly turns one query into many:&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;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetOrder&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;// query 1&lt;/span&gt;
&lt;span class="kt"&gt;var&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;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCustomer&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="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// query 2&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;task&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;// lazy-loaded...&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;.&lt;/span&gt;&lt;span class="n"&gt;Weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetWeight&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;.&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;// query 3, 4, 5, ... N&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logic moved and the data access multiplied. Moving logic out of the database doesn't move the data out of it. If you don't redesign the access along the way, you've built something slower and called it modern.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8ot3d1ljuvllswg4avy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8ot3d1ljuvllswg4avy.png" alt="Left: one thick arrow from app to database labeled " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the three things that broke one small screen are the three things that shape, and routinely break, a large migration: what runs beside the old path versus inside it, data that has to stay consistent across a gap that used to be a single transaction, and context that doesn't survive a boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do differently now
&lt;/h2&gt;

&lt;p&gt;I don't use AI less. Discovery is faster and the plans are better for it. What changed is where my attention goes.&lt;/p&gt;

&lt;p&gt;The model handles the code that's there. The part that's mine is the code that isn't: the assumptions written down nowhere, the configs that only exist in production, the context that rides along until a boundary drops it. So I spend less time on the new code and more on a single question, what assumption am I about to break, and where is it written down. It almost never is written down anywhere, which is exactly why it's still the part that needs a person.&lt;/p&gt;

&lt;p&gt;The feature was never where the risk lived. The risk lived in assumptions someone made years earlier and never expected to change. That's where I start looking now, not because the new code doesn't matter, but because the bugs were almost never in it.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>legacy</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
    <item>
      <title>Feature Flags Are Not a Free Lunch</title>
      <dc:creator>Prithwiraj Das</dc:creator>
      <pubDate>Thu, 02 Apr 2026 04:06:29 +0000</pubDate>
      <link>https://dev.to/prithwiraj_das_8bc02a0871/feature-flags-are-not-a-free-lunch-460b</link>
      <guid>https://dev.to/prithwiraj_das_8bc02a0871/feature-flags-are-not-a-free-lunch-460b</guid>
      <description>&lt;p&gt;Every article about trunk-based development eventually arrives at the same recommendation: use feature flags. Gate incomplete work behind a toggle. Merge to main freely. Turn features on when they are ready.&lt;/p&gt;

&lt;p&gt;I have implemented this. And over the past few years, I have watched it create a category of technical debt that nobody warned me about.&lt;/p&gt;

&lt;p&gt;This is not an argument against feature flags. They are a useful tool. But the advice to "just use feature flags" leaves out what happens six months later when nobody cleans them up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94swpnaaj97o8w4ceotr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94swpnaaj97o8w4ceotr.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The plan is always clean
&lt;/h2&gt;

&lt;p&gt;You are building a new feature. It is not ready for all users yet. So you wrap it behind a feature flag. During development, the flag is off in production. Your code lives in main, safely dormant.&lt;/p&gt;

&lt;p&gt;When the feature is ready, you turn the flag on. Once all users have adopted it and the old code path is no longer needed, you remove the flag.&lt;/p&gt;

&lt;p&gt;Development, release, adoption, cleanup. Four steps.&lt;/p&gt;

&lt;p&gt;In practice, step four almost never happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why flags become permanent
&lt;/h2&gt;

&lt;p&gt;The first time a feature flag sticks around longer than planned, it feels harmless.&lt;/p&gt;

&lt;p&gt;Client A adopted the new feature. Client B did not want to move yet. The flag stays on for A and off for B. No big deal. We will clean it up next quarter.&lt;/p&gt;

&lt;p&gt;But next quarter, a new feature gets built on top of the flag-on code path. Now removing the flag means untangling the new feature from the old toggle. The effort is not justified, so it gets pushed again.&lt;/p&gt;

&lt;p&gt;Meanwhile, Client C onboards and gets a third configuration. The flag is no longer a temporary gate. It is load-bearing infrastructure that was never designed to be permanent.&lt;/p&gt;

&lt;p&gt;I have worked on codebases where feature flags that were supposed to last a sprint were still active two years later. Not because anyone decided to keep them. Just because removing them required more effort than anyone could justify in a sprint cycle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl0mgothxfs1h559i6byn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl0mgothxfs1h559i6byn.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  One flag doing too much
&lt;/h2&gt;

&lt;p&gt;Feature flags start simple: on or off.&lt;/p&gt;

&lt;p&gt;Over time, they absorb responsibilities they were never meant to carry.&lt;/p&gt;

&lt;p&gt;In one project, a single feature flag controlled two completely unrelated things: the data source for a report and the visibility of certain columns in the UI. The flag was originally created for the data source migration. The column visibility logic was added later because it was "related enough" and creating a separate flag felt like overkill.&lt;/p&gt;

&lt;p&gt;When the team needed to decouple those behaviors for different clients, it turned into a multi-sprint effort. The simple toggle had become a load-bearing wall in the application logic.&lt;/p&gt;

&lt;p&gt;This pattern repeats. Flags accumulate side responsibilities because it is faster to add a condition to an existing flag than to create and manage a new one. Each addition makes the eventual cleanup harder.&lt;/p&gt;




&lt;h2&gt;
  
  
  The client matrix problem
&lt;/h2&gt;

&lt;p&gt;Feature flags become genuinely dangerous when combined with client-specific targeting.&lt;/p&gt;

&lt;p&gt;In a multi-tenant application, it is common to enable features per client. Client A gets the new engine. Client B stays on the old one. Client C gets the new engine but not the new reporting module. Each combination is a distinct application state.&lt;/p&gt;

&lt;p&gt;Now multiply that across several flags. Flag A controls data source. Flag B controls UI behavior. Flag C controls a calculation engine. Different clients have different combinations. The number of possible states grows fast.&lt;/p&gt;

&lt;p&gt;Nobody tests all of those combinations. The QA team tests the most common configurations. But an untested combination is a production incident waiting to happen.&lt;/p&gt;

&lt;p&gt;I have spent hours debugging issues that turned out to be caused by an unexpected interaction between two flags that were never designed to work together. The code was correct for each flag individually. It just had not been tested in that particular combination.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8wjpclvt4zj8qi6u6aud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8wjpclvt4zj8qi6u6aud.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Migrating flag platforms
&lt;/h2&gt;

&lt;p&gt;Here is something that should set off alarm bells: if your team is migrating from one feature flag platform to another, the flag sprawl has already gotten out of hand.&lt;/p&gt;

&lt;p&gt;I have been through one of these migrations. The stated goal was modernization and cost reduction. The unstated reason was that the old platform had accumulated so many stale, undocumented flags that nobody trusted it anymore.&lt;/p&gt;

&lt;p&gt;What actually happened: every active flag had to be recreated in the new platform. Every client-specific targeting rule had to be rebuilt. Every code reference had to be updated. Tickets were created just to promote individual flags from dev to staging to production. The migration took multiple sprints and added zero user-facing value.&lt;/p&gt;

&lt;p&gt;And the new platform? Within months, it started accumulating the same kind of sprawl. Because the problem was never the platform. It was the lack of lifecycle governance.&lt;/p&gt;




&lt;h2&gt;
  
  
  The naming problem
&lt;/h2&gt;

&lt;p&gt;When multiple teams create feature flags independently, naming conventions diverge fast.&lt;/p&gt;

&lt;p&gt;In one codebase, I saw flags named like &lt;code&gt;team-feature-enable&lt;/code&gt; alongside flags named like &lt;code&gt;project-component-updates&lt;/code&gt;. Different teams, different conventions. Some flags used feature names. Others used team abbreviations. There was no central registry and no way to look at a flag name and understand what it controlled without reading the code.&lt;/p&gt;

&lt;p&gt;A new developer picking up a ticket that involved feature flags had to trace each flag through the codebase to understand what it did, which clients it applied to, and whether it was still active. That detective work added hours to what should have been straightforward tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I think about now
&lt;/h2&gt;

&lt;p&gt;After living through flag chaos more than once, there are things I would set up before adopting feature flags at scale.&lt;/p&gt;

&lt;p&gt;Every flag gets a creation date, an owner, and an expected removal date. If the flag is still active past its expected date, it shows up somewhere visible and someone is accountable for it.&lt;/p&gt;

&lt;p&gt;One flag, one responsibility. If you are tempted to add a second behavior to an existing flag, create a new flag instead. The cost of managing an extra flag is far lower than untangling an overloaded one later.&lt;/p&gt;

&lt;p&gt;A naming convention before the first flag exists. The format matters less than the consistency.&lt;/p&gt;

&lt;p&gt;Dedicated time every quarter to audit active flags, remove stale ones, and consolidate duplicates. This never feels urgent, which is exactly why it needs to be scheduled.&lt;/p&gt;

&lt;p&gt;And if you have client-specific targeting, maintain a test matrix of the most common flag combinations. At minimum, document which combinations exist in production and make sure QA covers them.&lt;/p&gt;

&lt;p&gt;Feature flags are a useful tool. But without lifecycle discipline, they create the same kind of mess they were supposed to prevent. The chaos just moves to a different layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftuz8jk09a99tt96rr22q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftuz8jk09a99tt96rr22q.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>featureflags</category>
      <category>devops</category>
      <category>softwareengineering</category>
      <category>technicaldebt</category>
    </item>
    <item>
      <title>What Goes Wrong When You Switch Branching Strategies Mid-Flight</title>
      <dc:creator>Prithwiraj Das</dc:creator>
      <pubDate>Wed, 01 Apr 2026 19:50:27 +0000</pubDate>
      <link>https://dev.to/prithwiraj_das_8bc02a0871/what-goes-wrong-when-you-switch-branching-strategies-mid-flight-50l0</link>
      <guid>https://dev.to/prithwiraj_das_8bc02a0871/what-goes-wrong-when-you-switch-branching-strategies-mid-flight-50l0</guid>
      <description>&lt;p&gt;I have been part of at least four branching strategy migrations over a 12-year career.&lt;/p&gt;

&lt;p&gt;GitFlow to trunk-based. Bitbucket to GitHub. Sprint branches to release branches and back again.&lt;/p&gt;

&lt;p&gt;Every time, the decision to switch made sense on paper. Every time, the real problems showed up later, usually during the first production hotfix.&lt;/p&gt;

&lt;p&gt;Trunk-based development is the right long-term model for most teams. I am not arguing against it. But I have seen enough transitions go sideways that I wanted to write down what I have noticed, in case it is useful to someone going through the same thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6yue5t0l9hsh76oo5t4b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6yue5t0l9hsh76oo5t4b.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The switch usually happens fast
&lt;/h2&gt;

&lt;p&gt;In most places I have worked, the decision to change branching strategies came from leadership during a broader initiative. A platform migration, a new CI/CD tool, a push to modernize.&lt;/p&gt;

&lt;p&gt;The intent is always good. But the rollout tends to be uniform. Every repo switches at once, regardless of whether each team is ready for the new workflow.&lt;/p&gt;

&lt;p&gt;The thing is, a branching strategy is not really an infrastructure decision. It is a team capability decision. It works when the team's release cadence, test coverage, and deployment setup are aligned with it.&lt;/p&gt;

&lt;p&gt;When those things are not in place yet, the new model introduces problems that the old model, for all its flaws, did not have.&lt;/p&gt;




&lt;h2&gt;
  
  
  The test gap
&lt;/h2&gt;

&lt;p&gt;Trunk-based development assumes main is always releasable. That is a strong assumption.&lt;/p&gt;

&lt;p&gt;It only holds if automated tests run in the pipeline as a blocking gate.&lt;/p&gt;

&lt;p&gt;In more than one team I have been part of, tests were encouraged but not enforced in CI. Code could land in main without passing automated validation.&lt;/p&gt;

&lt;p&gt;In a branch-based model, that is survivable. Bad code sits in a feature or develop branch. You catch it before it reaches production.&lt;/p&gt;

&lt;p&gt;In trunk-based, there is no buffer. Main is production. If the pipeline does not block on test failures, you are relying on code review alone to catch runtime bugs and regressions.&lt;/p&gt;

&lt;p&gt;I have seen this pattern repeat across organizations. The branching model changes. The test infrastructure does not follow.&lt;/p&gt;

&lt;p&gt;The pipeline is the enforcement mechanism. If it does not fail on failing tests, those tests are effectively optional.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fok0q4hfhlx9sjov40k0d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fok0q4hfhlx9sjov40k0d.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The hotfix that could not ship
&lt;/h2&gt;

&lt;p&gt;This is where it gets real.&lt;/p&gt;

&lt;p&gt;A critical bug surfaces in production. The fix is reviewed and ready.&lt;/p&gt;

&lt;p&gt;But main has other changes in it that are not production-ready. Unfinished features, untested integrations. Deploying the fix means deploying everything.&lt;/p&gt;

&lt;p&gt;In theory, trunk-based prevents this because main should always be clean. In practice, that takes time to build: small commits, good CI, feature flags for incomplete work. During the transition period, main is rarely in that state.&lt;/p&gt;

&lt;p&gt;I have been in a situation where a straightforward hotfix took over 24 hours to unblock. The pipeline was not set up to deploy from anything other than main. The development team, the engineering manager, and the DevOps team all had to coordinate to add hotfix branch support on the spot.&lt;/p&gt;

&lt;p&gt;For teams working on business-critical applications, whether it is healthcare, logistics, or e-commerce, this is especially painful. Hotfixes are not rare events. Access control bugs, data display issues, calculation errors. These come up regularly and need to ship the same day.&lt;/p&gt;

&lt;p&gt;A branching strategy that turns a one-line fix into a coordination exercise has a gap that needs addressing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59pxka3g9ipceq5g5ywd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59pxka3g9ipceq5g5ywd.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The pipeline is not always in your hands
&lt;/h2&gt;

&lt;p&gt;Something that rarely comes up in branching strategy blog posts: in many organizations, developers do not control their own deployment pipelines.&lt;/p&gt;

&lt;p&gt;The CI/CD setup is owned by a DevOps or platform team. Developers can push code, but they cannot change workflow triggers, deployment rules, or environment settings.&lt;/p&gt;

&lt;p&gt;This is a reasonable separation of concerns. But it creates a coordination bottleneck during transitions.&lt;/p&gt;

&lt;p&gt;When the team I was on needed hotfix deployments added to the pipeline, it was not something we could do ourselves. It required a request to DevOps, who had to evaluate the change across dozens of repositories. That is a fair concern on their end. But on the development side, there was a fix ready to go and no way to ship it.&lt;/p&gt;

&lt;p&gt;Branch naming conventions added another layer. The pipeline required specific patterns like &lt;code&gt;feature/TICKET-ID&lt;/code&gt; to trigger builds. Push a branch with a slightly different format, and the pipeline did nothing. No error, no warning. Just silence.&lt;/p&gt;

&lt;p&gt;That kind of thing is easy to solve with documentation. But it catches people during the early days of a migration.&lt;/p&gt;

&lt;p&gt;When developers cannot see or influence how the pipeline behaves, the branching strategy on paper and the branching strategy in practice are two different things.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every repo ends up different
&lt;/h2&gt;

&lt;p&gt;One of the more disorienting things about a migration is that different repositories end up at different stages.&lt;/p&gt;

&lt;p&gt;Some repos followed a tag-based deployment model. Others deployed from main only. Some had manual dispatch. Others had strict branch naming triggers. The hotfix path that worked in one repo did not exist in another.&lt;/p&gt;

&lt;p&gt;During one incident, the tech lead mentioned that different architects had different expectations about the process. Some assumed one workflow, others assumed another. Everyone was working from a different mental model.&lt;/p&gt;

&lt;p&gt;Consistency matters more than having the perfect model. Three different "right" approaches across an organization is harder to manage than one imperfect approach that everyone understands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7whx1g5666w87xikmj3w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7whx1g5666w87xikmj3w.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I think about now
&lt;/h2&gt;

&lt;p&gt;After going through this a few times, there are things I look for before a branching strategy switch.&lt;/p&gt;

&lt;p&gt;Does the CI pipeline enforce tests as a hard blocker, not a suggestion?&lt;/p&gt;

&lt;p&gt;Is there a tested path to ship a hotfix to production without deploying everything in main?&lt;/p&gt;

&lt;p&gt;Can developers trigger common deployment operations without filing a ticket?&lt;/p&gt;

&lt;p&gt;Is the model consistent across repositories, or does each repo have its own workflow?&lt;/p&gt;

&lt;p&gt;Has anyone actually simulated a production hotfix under the new process?&lt;/p&gt;

&lt;p&gt;These are not exotic requirements. But in every migration I have been part of, at least a few of them were missing on day one.&lt;/p&gt;

&lt;p&gt;They always surfaced at the worst possible time.&lt;/p&gt;

&lt;p&gt;Trunk-based development is worth getting to. It just helps to make sure the foundation is there before making the switch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gbl1qyu2346260qhp2a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gbl1qyu2346260qhp2a.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>devops</category>
      <category>git</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>What broke my JWT flow in Blazor Server and how I fixed it</title>
      <dc:creator>Prithwiraj Das</dc:creator>
      <pubDate>Mon, 23 Mar 2026 15:10:13 +0000</pubDate>
      <link>https://dev.to/prithwiraj_das_8bc02a0871/what-broke-my-jwt-flow-in-blazor-server-and-how-i-fixed-it-105o</link>
      <guid>https://dev.to/prithwiraj_das_8bc02a0871/what-broke-my-jwt-flow-in-blazor-server-and-how-i-fixed-it-105o</guid>
      <description>&lt;p&gt;&lt;em&gt;Three things I got wrong building my first production Blazor Server app, coming from a background in Angular and traditional ASP.NET.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Originally published on &lt;a href="https://medium.com/@prithwirajdas07/what-broke-my-jwt-flow-in-blazor-server-and-how-i-fixed-it-50df948a64dd" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The one thing you need to understand first
&lt;/h2&gt;

&lt;p&gt;In a standard HTTP app, every user interaction is a new request — new pipeline, new HttpContext, new everything. Blazor Server works differently: the initial page load is an HTTP request, but after that, everything runs over a persistent WebSocket connection (SignalR). There are no further HTTP requests.&lt;/p&gt;

&lt;p&gt;This means &lt;strong&gt;HttpContext is only reliably available during that first request&lt;/strong&gt; — once the SignalR circuit is established, it is either null or stale.&lt;/p&gt;

&lt;p&gt;Microsoft's documentation puts it plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"IHttpContextAccessor generally should be avoided with interactive rendering because a valid HttpContext isn't always available."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one sentence explains everything below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Issue 1 — User metadata cached at the wrong time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What I had:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;UserMetadataService&lt;/code&gt; loaded user auth data in the constructor using &lt;code&gt;Task.Run(...).Wait()&lt;/code&gt;. Load once, reuse everywhere:&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;class&lt;/span&gt; &lt;span class="nc"&gt;UserMetadataService&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IHttpContextAccessor&lt;/span&gt; &lt;span class="n"&gt;_authState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IUserAuthData&lt;/span&gt; &lt;span class="n"&gt;userAuthData&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;private&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="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserMetadataService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpContextAccessor&lt;/span&gt; &lt;span class="n"&gt;authState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;_authState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authState&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;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoadUserMetaData&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;LoadUserMetaData&lt;/span&gt;&lt;span class="p"&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;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_authState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&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="n"&gt;userAuthData&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="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserAuthData&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="n"&gt;Claims&lt;/span&gt;&lt;span class="p"&gt;)&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="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;This worked on first load. But for users whose session context needed to be read after circuit establishment — role-based access checks, tenant resolution — the cached value was stale, and for some users it was null entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What fixed it:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Converting &lt;code&gt;userAuthData&lt;/code&gt; from a cached property to a computed one:&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;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserMetadataService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpContextAccessor&lt;/span&gt; &lt;span class="n"&gt;authState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IUserAuthData&lt;/span&gt; &lt;span class="n"&gt;userAuthData&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&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;new&lt;/span&gt; &lt;span class="nf"&gt;UserAuthData&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="n"&gt;Claims&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&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;h2&gt;
  
  
  Issue 2 — Reading HttpContext too late in TokenService
&lt;/h2&gt;

&lt;p&gt;This one took longer to find because it only failed intermittently and left almost nothing useful in the logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I had:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;TokenService&lt;/code&gt; held a reference to &lt;code&gt;IHttpContextAccessor&lt;/code&gt; and read &lt;code&gt;HttpContext&lt;/code&gt; inside &lt;code&gt;RefreshTokenAsync()&lt;/code&gt; — called whenever a new JWT was needed for an outgoing API request:&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;class&lt;/span&gt; &lt;span class="nc"&gt;TokenService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ITokenService&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IHttpContextAccessor&lt;/span&gt; &lt;span class="n"&gt;_httpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;RefreshTokenAsync&lt;/span&gt;&lt;span class="p"&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;httpContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_httpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&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;httpContext&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="n"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;IsAuthenticated&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
        &lt;span class="p"&gt;{&lt;/span&gt;  
            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"User is not authenticated"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
            &lt;span class="n"&gt;_cachedToken&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="n"&gt;_navigationManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NavigateTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_navigationManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forceLoad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
        &lt;span class="p"&gt;}&lt;/span&gt;  

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpContext&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tokenDescriptor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SecurityTokenDescriptor&lt;/span&gt;  
        &lt;span class="p"&gt;{&lt;/span&gt;  
            &lt;span class="n"&gt;Subject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ClaimsIdentity&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="n"&gt;Claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;  
            &lt;span class="n"&gt;IssuedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
            &lt;span class="n"&gt;Expires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The constructor runs during initial circuit setup, before SignalR takes over. That is the one reliable window to read from HttpContext in a Blazor Server service.&lt;/strong&gt; Reading it anywhere else — from a method, a timer, a component event — is not safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  Issue 3 — JwtTokenForwardHandler extending the wrong base class
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What I had:&lt;/strong&gt;&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;class&lt;/span&gt; &lt;span class="nc"&gt;JwtTokenForwardHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HttpClientHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="c1"&gt;// In Program.cs  &lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigurePrimaryHttpMessageHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceProvider&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
    &lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JwtTokenForwardHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HttpClientHandler&lt;/code&gt; is the primary transport handler — it owns the socket and the connection pool. Registering it via &lt;code&gt;ConfigurePrimaryHttpMessageHandler&lt;/code&gt; tells &lt;code&gt;IHttpClientFactory&lt;/code&gt; to treat it as the transport layer, which means it gets &lt;strong&gt;pooled and shared across requests&lt;/strong&gt;. Any user-specific state on that handler can persist across request boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What fixed it:&lt;/strong&gt;&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;class&lt;/span&gt; &lt;span class="nc"&gt;JwtTokenForwardHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DelegatingHandler&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ITokenService&lt;/span&gt; &lt;span class="n"&gt;_tokenService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;JwtTokenForwardHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ITokenService&lt;/span&gt; &lt;span class="n"&gt;tokenService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;_tokenService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&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;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
        &lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&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;token&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;_tokenService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentTokenAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;  
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
    &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;  

&lt;span class="c1"&gt;// In Program.cs  &lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHttpMessageHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JwtTokenForwardHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;DelegatingHandler&lt;/code&gt; is outgoing middleware — it sits in front of the transport layer, adds the token per call inside &lt;code&gt;SendAsync&lt;/code&gt;, and passes control down the chain. No state is stored on the handler instance.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I took away from this
&lt;/h2&gt;

&lt;p&gt;All three of these patterns felt correct when I wrote them. They were correct in every HTTP-based context I had used them in. The shift Blazor Server requires is not obvious from reading the API surface — it only becomes clear once you understand that the circuit model fundamentally changes when HttpContext exists and what service lifetimes mean in that environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional-scenarios#ihttpcontextaccessorhttpcontext-in-aspnet-core-blazor-apps" rel="noopener noreferrer"&gt;IHttpContextAccessor/HttpContext in ASP.NET Core Blazor apps — Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional-scenarios" rel="noopener noreferrer"&gt;ASP.NET Core server-side and Blazor Web App additional security scenarios — Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory" rel="noopener noreferrer"&gt;Use IHttpClientFactory — .NET | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory-troubleshooting" rel="noopener noreferrer"&gt;Troubleshoot IHttpClientFactory issues — .NET | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>dotnet</category>
      <category>blazor</category>
      <category>csharp</category>
    </item>
  </channel>
</rss>
