<?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: Vignesh Athiappan</title>
    <description>The latest articles on DEV Community by Vignesh Athiappan (@vicky_acedia).</description>
    <link>https://dev.to/vicky_acedia</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%2F4028899%2F1e712c2d-9953-4fd8-855f-a0bdd45cb21b.jpeg</url>
      <title>DEV Community: Vignesh Athiappan</title>
      <link>https://dev.to/vicky_acedia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vicky_acedia"/>
    <language>en</language>
    <item>
      <title>Promotable, Deployable, Rotatable: Making Bench Management an Actual Process</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Thu, 27 Aug 2026 13:57:52 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/promotable-deployable-rotatable-making-bench-management-an-actual-process-5b2a</link>
      <guid>https://dev.to/vicky_acedia/promotable-deployable-rotatable-making-bench-management-an-actual-process-5b2a</guid>
      <description>&lt;p&gt;Every services organisation has a bench — people between projects, waiting on a client start date, rolling off an engagement that ended early, or parked because the skill they have and the skill the market wants have drifted apart. The bench is not a defect. It's the cost of being able to say yes to work you haven't won yet.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; a defect is not knowing anything useful about the people on it.&lt;/p&gt;

&lt;p&gt;The usual state of affairs is a spreadsheet with a name, a skill string, a rate, and a date. That tells you who is unallocated. It tells you nothing about what to &lt;em&gt;do&lt;/em&gt; about it. So the conversation in the weekly review becomes a series of individual negotiations, driven by whoever in the room happens to remember something about that person. Decisions don't accumulate. Six weeks later you have the same conversation about the same person, and nobody can reconstruct why the last one went the way it did.&lt;/p&gt;

&lt;p&gt;PDR is one way out of that. It's a small idea, and most of the value is in the discipline it forces rather than the taxonomy itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three dispositions
&lt;/h2&gt;

&lt;p&gt;PDR classifies each person on the bench against three questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Promotable&lt;/strong&gt; — is this person ready to move up a level? Not "are they good," but: is there evidence they are already operating above their current band, and would a promotion make them easier to place at a higher billing rate?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployable&lt;/strong&gt; — can this person be put on billable work as-is, right now, with no intervention? This is the most misread of the three. Deployable is not a compliment. It means the skills match live demand and there is nothing blocking a start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotatable&lt;/strong&gt; — should this person move to a different technology, domain, or vertical? Rotation is the answer when someone is capable but their current specialisation has no pipeline behind it.&lt;/p&gt;

&lt;p&gt;The point of three separate axes is that they are not mutually exclusive and they are not a ranking. Someone can be promotable &lt;em&gt;and&lt;/em&gt; rotatable — a strong senior engineer in a dying stack. Someone can be deployable but not promotable — solid, well-matched, not ready for the next band. Someone can be none of the three, and that is the most important signal the framework produces, because it means the situation needs a decision that isn't "wait."&lt;/p&gt;

&lt;p&gt;The moment you collapse this into a single label — a tier, a grade, an A/B/C — you lose exactly the information you built the system to capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Intake
&lt;/h3&gt;

&lt;p&gt;Bench entries come from allocation data, not from manual entry. If someone has to remember to add a person, the list will be wrong within a week. The system should derive the bench from the absence of active billable allocation, then let humans annotate.&lt;/p&gt;

&lt;p&gt;Intake also needs a start date for the bench period. Aging is the single most predictive field you have, and it only works if the clock starts automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Assessment
&lt;/h3&gt;

&lt;p&gt;Someone with actual knowledge of the person marks the three flags. This is usually a delivery lead or a resource manager, not HR and not a tool.&lt;/p&gt;

&lt;p&gt;Two design decisions matter more than they sound:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unassessed must look different from assessed-and-negative.&lt;/strong&gt; "Not promotable" and "nobody has looked at this yet" are completely different facts, and a UI that renders both as an empty checkbox will quietly destroy your data quality. Blank means unknown. Show it as unfinished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every negative or unusual combination needs a justification.&lt;/strong&gt; Not a dropdown — free text, required, at the point of assessment. This is the gate that turns the framework from a labelling exercise into a decision record. It also slows people down, which is the intended effect. If marking someone as not deployable takes two seconds, it will be done thoughtlessly and often.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Action
&lt;/h3&gt;

&lt;p&gt;A classification with no downstream action is theatre. Each disposition should route somewhere concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Promotable → into the next promotion cycle with the justification attached as evidence&lt;/li&gt;
&lt;li&gt;Deployable → into active matching against open demand&lt;/li&gt;
&lt;li&gt;Rotatable → into a reskilling track with a named target skill and a timeline&lt;/li&gt;
&lt;li&gt;None of the above → escalation, with a review date&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fourth case is the one organisations avoid building. It's also the one that determines whether the whole exercise is honest.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Audit
&lt;/h3&gt;

&lt;p&gt;Every assessment change is appended, never overwritten. Who changed what, when, from what to what, and why. Append-only, no edits, no deletes.&lt;/p&gt;

&lt;p&gt;This is non-negotiable for two reasons. The obvious one is that these records touch promotion, reassignment, and eventually separation decisions, and you will be asked to explain them. The less obvious one is that the history is where the actual insight lives. A person marked rotatable four quarters running, with four justifications, is telling you something about your reskilling programme, not about the person.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually goes wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coverage gets measured instead of accuracy.&lt;/strong&gt; "94% of the bench is assessed" is a number that goes up when people click things. It says nothing about whether the assessments are right. Coverage is a hygiene metric — worth a single indicator, not a dashboard. If your reporting makes coverage the headline, you have built an incentive to fill in boxes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployable becomes the default.&lt;/strong&gt; Marking someone deployable is the path of least resistance: it's positive, it needs no justification, it moves the problem to the matching team. Watch the distribution. If deployable is running above 70% of assessed bench while placement rates stay flat, the flag has stopped meaning anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assessments go stale.&lt;/strong&gt; A three-month-old assessment is a guess. Build in expiry — an assessment older than a defined window reverts to unassessed rather than continuing to display as fact. People will hate this. Do it anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotation is recommended without capacity to deliver it.&lt;/strong&gt; Marking someone rotatable is free. Actually retraining them costs money, bench time, and a mentor. If rotation recommendations exceed reskilling capacity by 5x, the flag is a way of deferring a harder conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The framework gets used as a performance tool.&lt;/strong&gt; PDR describes &lt;em&gt;market fit and placement readiness&lt;/em&gt;. It is not a performance rating, and the two must not be joined. A high performer in a technology with no demand is not deployable, and if your system lets that read as a performance signal, you will lose good people and deserve to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest framing
&lt;/h2&gt;

&lt;p&gt;PDR is a decision-forcing device, not an optimisation engine. It won't tell you what to do with anyone. What it does is make the absence of a decision visible — an unassessed row, a stale assessment, a person marked rotatable for three quarters with no training assigned. That visibility is uncomfortable, which is the entire point, and it's also the reason most implementations quietly drift toward measuring completion rates instead.&lt;/p&gt;

&lt;p&gt;If you're building something like this, the test isn't whether the dashboard looks good. It's whether the weekly bench review is shorter and produces fewer repeat conversations than it did before. If a person's situation comes up twice with no change in between, the system failed regardless of how much of the bench is "covered."&lt;/p&gt;

&lt;p&gt;One last thing worth saying plainly: these are people, and the labels are blunt. "Not deployable" is a statement about a market, a skills pipeline, and a sales pipeline — three things the individual mostly doesn't control. Build the justification field wide, require it, and read what people write in it. That text is usually a more accurate description of your organisation's problems than of the person's.&lt;/p&gt;

</description>
      <category>process</category>
    </item>
    <item>
      <title>CI/CD for Azure Logic Apps Standard on a Private (ILB) ASE — Without a VNet Agent</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Wed, 26 Aug 2026 04:19:58 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/cicd-for-azure-logic-apps-standard-on-a-private-ilb-ase-without-a-vnet-agent-1j2e</link>
      <guid>https://dev.to/vicky_acedia/cicd-for-azure-logic-apps-standard-on-a-private-ilb-ase-without-a-vnet-agent-1j2e</guid>
      <description>&lt;p&gt;&lt;em&gt;How we wired an Azure DevOps pipeline to a Logic App Standard running on an internal App Service Environment, hit six real-world failures on the way, and ended with a deployment that needs no SAS tokens, no SCM access, and no VNet-connected build agent.&lt;/em&gt;&lt;/p&gt;




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

&lt;p&gt;Logic Apps Standard on an &lt;strong&gt;ILB App Service Environment (ASE v3)&lt;/strong&gt; has no public SCM (Kudu) endpoint. That kills the two "normal" deployment routes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Why it fails on ILB ASE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;az webapp deploy&lt;/code&gt; / zip-deploy from a Microsoft-hosted agent&lt;/td&gt;
&lt;td&gt;Talks to the SCM endpoint, which is private&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code / portal publish&lt;/td&gt;
&lt;td&gt;Same SCM dependency, plus it isn't CI/CD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The usual answer is "stand up a self-hosted agent inside the VNet." That's the right long-term answer — but you can ship &lt;strong&gt;today&lt;/strong&gt; without it, using &lt;strong&gt;Run-From-Package with a managed-identity-authenticated blob URL&lt;/strong&gt;. The pipeline only ever talks to two public planes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Azure Storage&lt;/strong&gt; (upload the package)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARM / management.azure.com&lt;/strong&gt; (set one app setting, restart the app)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The app then pulls the package itself, from inside the VNet, using its own managed identity. SCM is never touched. No SAS token ever exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git push ──▶ ADO pipeline ──▶ zip ──▶ Blob Storage
                                          ▲
                                          │ (pull via app's
                                          │  managed identity)
             ARM: set app setting ──▶ Logic App (ILB ASE)
             ARM: restart app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What you need before starting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Prerequisite&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Logic App Standard resource, &lt;strong&gt;Started&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;A stopped app silently accepts settings and deploys nothing. Check the Overview blade — ours had been sitting in &lt;code&gt;Stopped&lt;/code&gt; and cost us a confused hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;A storage account the ASE can reach&lt;/td&gt;
&lt;td&gt;Same region as the ASE. Public network access enabled is the simple path; a private endpoint works too&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;A blob container, private access&lt;/td&gt;
&lt;td&gt;e.g. &lt;code&gt;deployments&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;An ADO agent (hosted or self-hosted) with &lt;strong&gt;Azure CLI installed&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Ours was a Windows self-hosted box that did &lt;em&gt;not&lt;/em&gt; have &lt;code&gt;az&lt;/code&gt; — see failure #3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;An ARM service connection&lt;/td&gt;
&lt;td&gt;Any auth type works. If it's the &lt;strong&gt;"Managed identity (agent-assigned)"&lt;/strong&gt; type, the identity must actually be enabled on the agent VM — see failure #2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Rights to create role assignments on the storage account and the Logic App's resource group&lt;/td&gt;
&lt;td&gt;Or a friendly admin&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 1 — Repository layout
&lt;/h2&gt;

&lt;p&gt;One folder per Logic App Standard resource; one subfolder per workflow. The zip we deploy is simply the app folder's contents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;logicapps-platform/&lt;/span&gt;
&lt;span class="s"&gt;├── apps/&lt;/span&gt;
&lt;span class="s"&gt;│   └── la-integration-prod/          ← 1 folder = 1 Logic App Standard resource&lt;/span&gt;
&lt;span class="s"&gt;│       ├── host.json                 ← runtime + extension bundle&lt;/span&gt;
&lt;span class="s"&gt;│       ├── connections.json          ← connector references ({} to start)&lt;/span&gt;
&lt;span class="s"&gt;│       ├── parameters.json           ← per-environment values ({} to start)&lt;/span&gt;
&lt;span class="s"&gt;│       ├── .funcignore&lt;/span&gt;
&lt;span class="s"&gt;│       └── Heartbeat-Test/           ← 1 folder = 1 workflow&lt;/span&gt;
&lt;span class="s"&gt;│           └── workflow.json&lt;/span&gt;
&lt;span class="s"&gt;└── pipelines/&lt;/span&gt;
    &lt;span class="s"&gt;└── deploy-la-integration-prod.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;host.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extensionBundle"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Azure.Functions.ExtensionBundle.Workflows"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[1.*, 2.0.0)"&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;&lt;code&gt;connections.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"managedApiConnections"&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;"serviceProviderConnections"&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="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;&lt;code&gt;Heartbeat-Test/workflow.json&lt;/code&gt; — a deliberately trivial pilot workflow. No connectors, no secrets, nothing that can fail for reasons unrelated to the pipeline. Prove the pipe first; migrate real workflows second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"definition"&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"contentVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"triggers"&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;"Every_Hour"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recurrence"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"recurrence"&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;"frequency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hour"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"interval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"actions"&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;"Compose_Heartbeat"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Compose"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deployed via ADO pipeline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"runAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@{utcNow()}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"workflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@{workflow().name}"&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;"runAfter"&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="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;"outputs"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stateful"&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Path gotcha:&lt;/strong&gt; if your files live in a subfolder of the repo (e.g. &lt;code&gt;myrepo/logicapps-platform/apps/...&lt;/code&gt;), the pipeline's &lt;code&gt;sourceFolder&lt;/code&gt; variable and trigger paths must include that prefix. Our first run failed with &lt;code&gt;Cannot find path ...\apps\&lt;/code&gt; for exactly this reason.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2 — Identity and role assignments
&lt;/h2&gt;

&lt;p&gt;Three grants total. Two for the &lt;strong&gt;pipeline's identity&lt;/strong&gt; (whatever your service connection authenticates as), one for the &lt;strong&gt;Logic App's own identity&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2a. Pipeline identity
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Storage Blob Data Contributor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The storage account&lt;/td&gt;
&lt;td&gt;Upload the package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Website Contributor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The Logic App's resource group&lt;/td&gt;
&lt;td&gt;Set app settings + restart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"Storage Blob Data Contributor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee-object-id&lt;/span&gt; &amp;lt;pipeline-identity-object-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee-principal-type&lt;/span&gt; ServicePrincipal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"/subscriptions/&amp;lt;sub-id&amp;gt;/resourceGroups/&amp;lt;storage-rg&amp;gt;/providers/Microsoft.Storage/storageAccounts/&amp;lt;storage-account&amp;gt;"&lt;/span&gt;

az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"Website Contributor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee-object-id&lt;/span&gt; &amp;lt;pipeline-identity-object-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee-principal-type&lt;/span&gt; ServicePrincipal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"/subscriptions/&amp;lt;sub-id&amp;gt;/resourceGroups/&amp;lt;logicapp-rg&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How to find the pipeline identity's object ID when nobody remembers it:&lt;/strong&gt; run a throwaway pipeline on the target pool that asks the instance metadata service directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SelfHosted-Windows-Pool'&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;powershell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;$r = Invoke-RestMethod -Headers @{Metadata="true"} -Uri "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&amp;amp;resource=https://management.azure.com/"&lt;/span&gt;
    &lt;span class="s"&gt;$p = $r.access_token.Split('.')[1].Replace('-','+').Replace('_','/')&lt;/span&gt;
    &lt;span class="s"&gt;switch ($p.Length % 4) { 2 {$p+='=='}; 3 {$p+='='} }&lt;/span&gt;
    &lt;span class="s"&gt;$c = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($p)) | ConvertFrom-Json&lt;/span&gt;
    &lt;span class="s"&gt;Write-Host "objectId : $($c.oid)"&lt;/span&gt;
    &lt;span class="s"&gt;Write-Host "resource : $($c.xms_mirid)"   # full ARM ID of the VM - tells you where it lives&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Who am I&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns &lt;code&gt;Identity not found&lt;/code&gt;, the agent VM's system-assigned identity is &lt;strong&gt;off&lt;/strong&gt; — enable it (VM → Identity → System assigned → On) and note the object ID it produces. This exact thing happened to us: the MI-type service connection had existed for months and had &lt;em&gt;never worked&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2b. Logic App identity (the key to SAS-free deployment)
&lt;/h3&gt;

&lt;p&gt;Enable the app's system-assigned identity (Logic App → Identity → System assigned → On), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az role assignment create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"Storage Blob Data Reader"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee-object-id&lt;/span&gt; &amp;lt;logicapp-identity-object-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assignee-principal-type&lt;/span&gt; ServicePrincipal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"/subscriptions/&amp;lt;sub-id&amp;gt;/resourceGroups/&amp;lt;storage-rg&amp;gt;/providers/Microsoft.Storage/storageAccounts/&amp;lt;storage-account&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;RBAC propagation is real.&lt;/strong&gt; Allow ~5 minutes between creating a role assignment and testing it. Restarting the app 30 seconds after the grant gives a false failure.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 3 — The pipeline
&lt;/h2&gt;

&lt;p&gt;The complete, working YAML. Written for a &lt;strong&gt;Windows&lt;/strong&gt; agent (all steps PowerShell); on a Linux agent, switch &lt;code&gt;scriptType: ps&lt;/code&gt; to &lt;code&gt;bash&lt;/code&gt; and adjust the validation step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ============================================================================&lt;/span&gt;
&lt;span class="c1"&gt;# Deploy Logic App Standard via Run-From-Package (Blob, MI-authenticated URL)&lt;/span&gt;
&lt;span class="c1"&gt;# The app pulls the package with ITS OWN managed identity: no SAS, no expiry,&lt;/span&gt;
&lt;span class="c1"&gt;# and the SCM endpoint is never touched, so an ILB ASE is not a problem.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# PREREQS:&lt;/span&gt;
&lt;span class="c1"&gt;#   - Logic App system-assigned identity ON, with 'Storage Blob Data Reader'&lt;/span&gt;
&lt;span class="c1"&gt;#     on the storage account&lt;/span&gt;
&lt;span class="c1"&gt;#   - Pipeline identity: 'Storage Blob Data Contributor' on the storage&lt;/span&gt;
&lt;span class="c1"&gt;#     account and 'Website Contributor' on the Logic App's resource group&lt;/span&gt;
&lt;span class="c1"&gt;# ============================================================================&lt;/span&gt;

&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apps/la-integration-prod/**&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pipelines/**&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SelfHosted-Windows-Pool'&lt;/span&gt;     &lt;span class="c1"&gt;# or vmImage: 'ubuntu-latest' for SP-auth connections&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;serviceConnection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-azure-service-connection'&lt;/span&gt;
  &lt;span class="na"&gt;resourceGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rg-logicapps-prod'&lt;/span&gt;
  &lt;span class="na"&gt;appName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;           &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;la-integration-prod'&lt;/span&gt;
  &lt;span class="na"&gt;storageAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stdeploypkgs'&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;         &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deployments'&lt;/span&gt;
  &lt;span class="na"&gt;sourceFolder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apps/la-integration-prod'&lt;/span&gt;
  &lt;span class="na"&gt;packageName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;       &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;la-integration-prod-$(Build.BuildId).zip'&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="c1"&gt;# ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate &amp;amp; package&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Package&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;powershell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;$bad = 0&lt;/span&gt;
        &lt;span class="s"&gt;Get-ChildItem -Path "$(sourceFolder)" -Recurse -Filter *.json | ForEach-Object {&lt;/span&gt;
          &lt;span class="s"&gt;$raw = Get-Content $_.FullName -Raw&lt;/span&gt;
          &lt;span class="s"&gt;try { $raw | ConvertFrom-Json | Out-Null }&lt;/span&gt;
          &lt;span class="s"&gt;catch { Write-Host "##vso[task.logissue type=error]Invalid JSON: $($_.FullName)"; $bad++ }&lt;/span&gt;
          &lt;span class="s"&gt;if ($raw -match '"(secret|client_secret|clientSecret|password)"\s*:\s*"(?!@)') {&lt;/span&gt;
            &lt;span class="s"&gt;Write-Host "##vso[task.logissue type=error]Inline secret in: $($_.FullName)"; $bad++&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;if ($bad -gt 0) { exit 1 }&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate JSON + block inline secrets&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ArchiveFiles@2&lt;/span&gt;
      &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Zip app content&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;rootFolderOrFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(sourceFolder)'&lt;/span&gt;
        &lt;span class="na"&gt;includeRootFolder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;          &lt;span class="c1"&gt;# host.json must sit at the ZIP ROOT&lt;/span&gt;
        &lt;span class="na"&gt;archiveType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zip&lt;/span&gt;
        &lt;span class="na"&gt;archiveFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)/$(packageName)'&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)/$(packageName)'&lt;/span&gt;
      &lt;span class="na"&gt;artifact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;package&lt;/span&gt;

&lt;span class="c1"&gt;# ----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to $(appName)&lt;/span&gt;
  &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;logicapps-prod'&lt;/span&gt;         &lt;span class="c1"&gt;# add an approval gate on this environment&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runOnce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;download&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;current&lt;/span&gt;
            &lt;span class="na"&gt;artifact&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;package&lt;/span&gt;

          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureCLI@2&lt;/span&gt;
            &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload package to blob&lt;/span&gt;
            &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(serviceConnection)'&lt;/span&gt;
              &lt;span class="na"&gt;scriptType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ps&lt;/span&gt;
              &lt;span class="na"&gt;scriptLocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inlineScript&lt;/span&gt;
              &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                &lt;span class="s"&gt;az storage blob upload `&lt;/span&gt;
                  &lt;span class="s"&gt;--account-name $(storageAccount) `&lt;/span&gt;
                  &lt;span class="s"&gt;--container-name $(container) `&lt;/span&gt;
                  &lt;span class="s"&gt;--name $(packageName) `&lt;/span&gt;
                  &lt;span class="s"&gt;--file "$(Pipeline.Workspace)/package/$(packageName)" `&lt;/span&gt;
                  &lt;span class="s"&gt;--auth-mode login --overwrite&lt;/span&gt;

          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureCLI@2&lt;/span&gt;
            &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Point app at package (MI-auth URL, no SAS, no SCM)&lt;/span&gt;
            &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(serviceConnection)'&lt;/span&gt;
              &lt;span class="na"&gt;scriptType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ps&lt;/span&gt;
              &lt;span class="na"&gt;scriptLocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inlineScript&lt;/span&gt;
              &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                &lt;span class="s"&gt;$ErrorActionPreference = "Stop"&lt;/span&gt;
                &lt;span class="s"&gt;$url = "https://$(storageAccount).blob.core.windows.net/$(container)/$(packageName)"&lt;/span&gt;

                &lt;span class="s"&gt;az webapp config appsettings set `&lt;/span&gt;
                  &lt;span class="s"&gt;-g "$(resourceGroup)" -n "$(appName)" `&lt;/span&gt;
                  &lt;span class="s"&gt;--settings WEBSITE_RUN_FROM_PACKAGE="$url" WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID="SystemAssigned" `&lt;/span&gt;
                  &lt;span class="s"&gt;-o none&lt;/span&gt;

                &lt;span class="s"&gt;az webapp restart -g "$(resourceGroup)" -n "$(appName)"&lt;/span&gt;
                &lt;span class="s"&gt;Write-Host "Deployed $(packageName). App restarted."&lt;/span&gt;

          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureCLI@2&lt;/span&gt;
            &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify workflow exists&lt;/span&gt;
            &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;azureSubscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(serviceConnection)'&lt;/span&gt;
              &lt;span class="na"&gt;scriptType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ps&lt;/span&gt;
              &lt;span class="na"&gt;scriptLocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inlineScript&lt;/span&gt;
              &lt;span class="na"&gt;inlineScript&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                &lt;span class="s"&gt;Start-Sleep -Seconds 60&lt;/span&gt;
                &lt;span class="s"&gt;$sub = az account show --query id -o tsv&lt;/span&gt;
                &lt;span class="s"&gt;az rest --method GET `&lt;/span&gt;
                  &lt;span class="s"&gt;--uri "https://management.azure.com/subscriptions/$sub/resourceGroups/$(resourceGroup)/providers/Microsoft.Web/sites/$(appName)/workflows?api-version=2022-03-01" `&lt;/span&gt;
                  &lt;span class="s"&gt;--query "value[].name" -o tsv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines carry the whole trick:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;WEBSITE_RUN_FROM_PACKAGE&lt;/code&gt; = the &lt;strong&gt;plain&lt;/strong&gt; blob URL — no SAS query string.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID=SystemAssigned&lt;/code&gt; — tells App Service to fetch the package using the app's own managed identity. &lt;strong&gt;Without this setting the runtime attempts anonymous access, fails, and the app reports &lt;code&gt;ServiceUnavailable&lt;/code&gt; from the host runtime.&lt;/strong&gt; This is the single most-missed step.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;includeRootFolder: false&lt;/code&gt; in the archive task — &lt;code&gt;host.json&lt;/code&gt; must be at the root of the zip, not nested one level down.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 4 — Run it, and what "success" looks like
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create the pipeline from the YAML, run it. First run will pause to ask permission on the service connection / environment / pool — click &lt;strong&gt;Permit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Green pipeline, and the Verify step prints your workflow name(s).&lt;/li&gt;
&lt;li&gt;Portal → Logic App → &lt;strong&gt;Workflows&lt;/strong&gt; shows the workflow, &lt;code&gt;Stateful&lt;/code&gt;, &lt;code&gt;Enabled&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open it → &lt;strong&gt;Run history&lt;/strong&gt; → confirm a run actually executed with the expected output. A workflow that loads but never runs is not success.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Everything that actually went wrong (the useful part)
&lt;/h2&gt;

&lt;p&gt;Our path to green, in order. If you're debugging, scan this table first.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Unable to locate executable file: 'bash'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Assumed Linux; the self-hosted agent was &lt;strong&gt;Windows&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;All steps → PowerShell (&lt;code&gt;scriptType: ps&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Invoke-RestMethod : {"error":"invalid_request","error_description":"Identity not found"}&lt;/code&gt; on the metadata endpoint&lt;/td&gt;
&lt;td&gt;Agent VM's system-assigned managed identity was &lt;strong&gt;off&lt;/strong&gt; — the MI-type service connection had never actually worked&lt;/td&gt;
&lt;td&gt;VM → Identity → System assigned → On. If the identity was enabled after boot, a VM restart may be needed before the token endpoint responds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Azure CLI 2.x is not installed on this machine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fresh agent VM, no &lt;code&gt;az&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Install via portal &lt;strong&gt;Run Command&lt;/strong&gt; (no RDP needed), then restart the agent service so it picks up the new PATH:&lt;br&gt;&lt;code&gt;Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile C:\az.msi&lt;/code&gt;&lt;br&gt;&lt;code&gt;Start-Process msiexec.exe -ArgumentList '/i C:\az.msi /quiet /norestart' -Wait&lt;/code&gt;&lt;br&gt;`Get-Service vstsagent* \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;{% raw %}&lt;code&gt;Get-ChildItem : Cannot find path '...\apps\'&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Repo files nested one folder deeper than the YAML expected&lt;/td&gt;
&lt;td&gt;Fix &lt;code&gt;sourceFolder&lt;/code&gt; and trigger &lt;code&gt;paths&lt;/code&gt; to include the prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ERROR: incorrect usage: --expiry should be within 7 days from now&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identity-based (user-delegation) SAS is capped at &lt;strong&gt;7 days&lt;/strong&gt; — and a 7-day SAS would be a time bomb anyway, since the app re-reads the package URL on every restart&lt;/td&gt;
&lt;td&gt;Drop SAS entirely; switch to the MI-authenticated plain URL (this article's approach)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Pipeline green, but portal shows &lt;code&gt;Error retrieving workflows. Encountered an error (ServiceUnavailable) from host runtime&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;App couldn't pull the package: &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID&lt;/code&gt; missing, &lt;strong&gt;and&lt;/strong&gt; the app's identity had no role on storage&lt;/td&gt;
&lt;td&gt;Add the app setting; grant &lt;code&gt;Storage Blob Data Reader&lt;/code&gt; to the app's identity; wait ~5 min for RBAC; restart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  A 60-second diagnostic when the runtime won't start
&lt;/h3&gt;

&lt;p&gt;Run from any machine with &lt;code&gt;az&lt;/code&gt; — checks the four usual suspects at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$rg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rg-logicapps-prod"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"la-integration-prod"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$st&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"stdeploypkgs"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$strg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rg-storage"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sub&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;sub-id&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 1. Identity on?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;webapp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;principalId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 2. Both settings present?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;webapp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;appsettings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[?starts_with(name,'WEBSITE_RUN_FROM')].{n:name,v:value}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 3. App identity has a role on storage?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--assignee&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;principalId-from-step-1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/subscriptions/&lt;/span&gt;&lt;span class="nv"&gt;$sub&lt;/span&gt;&lt;span class="s2"&gt;/resourceGroups/&lt;/span&gt;&lt;span class="nv"&gt;$strg&lt;/span&gt;&lt;span class="s2"&gt;/providers/Microsoft.Storage/storageAccounts/&lt;/span&gt;&lt;span class="nv"&gt;$st&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[].roleDefinitionName"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 4. Storage reachable at all?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$strg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$st&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{publicNetworkAccess:publicNetworkAccess, defaultAction:networkRuleSet.defaultAction}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whichever check comes back empty is your culprit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this beats the alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SAS URL in &lt;code&gt;WEBSITE_RUN_FROM_PACKAGE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Works, but user-delegation SAS caps at 7 days and account-key SAS means handling keys. Either way the app dies quietly when the token expires — on whatever future day it happens to restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zip-deploy via VNet self-hosted agent&lt;/td&gt;
&lt;td&gt;The classic answer. Fine — but it makes deployment hostage to one VM's health, and you still shouldn't need SCM for a package-based app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MI-authenticated Run-From-Package&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No secrets, no expiry, no SCM, works from any agent that can reach ARM + Storage. The package is also immutable per build ID, so rollback = point the setting at the previous zip and restart&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hardening checklist for after the pilot
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Approval gate on the ADO Environment before the Deploy stage.&lt;/li&gt;
&lt;li&gt;Private endpoint on the storage account once the pattern is proven (the app pulls over the VNet; the pipeline needs its own route — service endpoint or an agent with access).&lt;/li&gt;
&lt;li&gt;Lifecycle policy on the container — build-numbered zips accumulate forever otherwise.&lt;/li&gt;
&lt;li&gt;Keep the inline-secret linter. It costs nothing and blocks the exact class of mistake that infests legacy workflow definitions.&lt;/li&gt;
&lt;li&gt;New workflows that replace still-running legacy ones should ship &lt;strong&gt;disabled&lt;/strong&gt; and be enabled at cutover — never let two schedulers fire the same job.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Total elapsed time for us, including all six failures: one working session. Total time if you follow this article: about 30 minutes — 25 of which are waiting for an MSI installer and RBAC propagation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>logicapps</category>
      <category>azure</category>
    </item>
    <item>
      <title># CSPM, CWPP, CIEM, CNAPP, EASM: Decoding the Cloud Security Alphabet</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Sat, 22 Aug 2026 04:30:21 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/-cspm-cwpp-ciem-cnapp-easm-decoding-the-cloud-security-alphabet-2849</link>
      <guid>https://dev.to/vicky_acedia/-cspm-cwpp-ciem-cnapp-easm-decoding-the-cloud-security-alphabet-2849</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of 4 in the "Security Testing Landscape" series&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The cloud security market has produced some of the worst acronyms in IT history. Here's the secret that makes them all click: &lt;strong&gt;these aren't tests — they're continuous monitoring platforms, and each one watches a different layer of your cloud.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The analogy that makes it permanent: &lt;strong&gt;your cloud is a house.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CSPM — Cloud Security Posture Management
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Are the doors and windows locked?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;CSPM watches your cloud &lt;strong&gt;configuration&lt;/strong&gt;: public storage accounts, wide-open network security groups, disabled encryption, subscriptions without MFA. It continuously scans against benchmarks (CIS, well-architected frameworks) and flags drift the moment someone loosens a setting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answers:&lt;/strong&gt; "Is my cloud set up wrong?"&lt;br&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; Microsoft Defender for Cloud, Wiz, Prisma Cloud, Orca&lt;/p&gt;

&lt;h2&gt;
  
  
  CWPP — Cloud Workload Protection Platform
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Is there an intruder inside a room?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;CWPP watches the &lt;strong&gt;workloads themselves&lt;/strong&gt; — VMs, containers, serverless functions: vulnerabilities inside them, malware, suspicious runtime behavior, file integrity changes. Think of it as EDR, but for cloud compute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answers:&lt;/strong&gt; "Is something bad running inside my machines?"&lt;br&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; Defender for Servers/Containers, CrowdStrike Falcon Cloud Security, Aqua, SentinelOne&lt;/p&gt;

&lt;h2&gt;
  
  
  CIEM — Cloud Infrastructure Entitlement Management
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Who has too many keys?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;CIEM watches &lt;strong&gt;identities and permissions&lt;/strong&gt;: which users, service principals, and managed identities can do what. It hunts over-permissioned accounts, roles nobody has used in months, and toxic combinations like "this identity can read the secrets vault &lt;em&gt;and&lt;/em&gt; is reachable from the internet."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answers:&lt;/strong&gt; "Who can do too much?"&lt;br&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; Microsoft Entra Permissions Management, Wiz, Sonrai, Tenable (Ermetic)&lt;/p&gt;

&lt;h2&gt;
  
  
  CNAPP — Cloud-Native Application Protection Platform
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The whole smart-home security system.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the trick: CNAPP is &lt;strong&gt;not a new capability&lt;/strong&gt;. It's the umbrella that bundles CSPM + CWPP + CIEM (plus IaC scanning and container scanning) into one platform with one dashboard. Its real power is &lt;strong&gt;chaining findings into attack paths&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;vulnerable container &lt;strong&gt;+&lt;/strong&gt; public exposure &lt;strong&gt;+&lt;/strong&gt; admin identity &lt;strong&gt;=&lt;/strong&gt; critical attack path, fix this first&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That prioritization — attack paths instead of ten thousand isolated alerts — is why the entire market converged on CNAPP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answers:&lt;/strong&gt; "Show me the actual attack paths across everything."&lt;br&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; Wiz, Prisma Cloud, Orca, Defender for Cloud (full plans) — the same vendors, selling the full suite&lt;/p&gt;

&lt;h2&gt;
  
  
  EASM — External Attack Surface Management
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Walking around the street outside, checking what a burglar sees.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;EASM flips the camera: it scans your organization &lt;strong&gt;from the public internet&lt;/strong&gt;, like an attacker with a browser and patience. It routinely finds assets you forgot existed — abandoned subdomains, exposed test APIs, expired certificates, shadow IT spun up by a team three years ago.&lt;/p&gt;

&lt;p&gt;The uncomfortable, recurring outcome: EASM discovers things that aren't in your asset inventory at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answers:&lt;/strong&gt; "What can the internet see of mine?"&lt;br&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; Microsoft Defender EASM, Censys, Palo Alto Cortex Xpanse — and Shodan for the manual version&lt;/p&gt;

&lt;h2&gt;
  
  
  The House, One Last Time
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Acronym&lt;/th&gt;
&lt;th&gt;Watches&lt;/th&gt;
&lt;th&gt;House analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSPM&lt;/td&gt;
&lt;td&gt;Settings/config&lt;/td&gt;
&lt;td&gt;Doors and windows locked?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CWPP&lt;/td&gt;
&lt;td&gt;Workloads&lt;/td&gt;
&lt;td&gt;Intruder inside a room?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CIEM&lt;/td&gt;
&lt;td&gt;Identities/permissions&lt;/td&gt;
&lt;td&gt;Who has too many keys?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CNAPP&lt;/td&gt;
&lt;td&gt;All of the above&lt;/td&gt;
&lt;td&gt;The full smart-home system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EASM&lt;/td&gt;
&lt;td&gt;The outside view&lt;/td&gt;
&lt;td&gt;What the burglar sees from the street&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One final classification note: CSPM, CWPP, CIEM, and CNAPP are all &lt;strong&gt;white box&lt;/strong&gt; — they read your subscriptions with granted access. &lt;strong&gt;EASM is the lone black box&lt;/strong&gt; of the group, and that outsider's perspective is exactly what makes it valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Wrap-Up
&lt;/h2&gt;

&lt;p&gt;Across four posts, the entire landscape reduces to four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is my application code safe?&lt;/strong&gt; → App Security Testing (SAST, DAST, SCA…)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can someone actually break in?&lt;/strong&gt; → Offensive assessments (VAPT, red teams, bug bounties)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are we designing, building, and proving it right?&lt;/strong&gt; → Reviews and audits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is my cloud continuously watched?&lt;/strong&gt; → CSPM/CNAPP family&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Master which question each acronym answers, and you'll never nod along blankly in a security meeting again.&lt;/p&gt;

</description>
      <category>security</category>
    </item>
    <item>
      <title>The Security Work That Never Attacks Anything: Reviews, Audits &amp; Threat Modeling</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Sat, 22 Aug 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/the-security-work-that-never-attacks-anything-reviews-audits-threat-modeling-1pda</link>
      <guid>https://dev.to/vicky_acedia/the-security-work-that-never-attacks-anything-reviews-audits-threat-modeling-1pda</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of 4 in the "Security Testing Landscape" series&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The previous two posts covered tools and humans that attack systems. This group is different: &lt;strong&gt;nobody attacks anything&lt;/strong&gt;. These are humans (and a few platforms) inspecting your designs, code, configurations, and paperwork — with full access. Every single item here is white box; that's the group's signature.&lt;/p&gt;

&lt;p&gt;The best way to remember them is by the &lt;strong&gt;question each one answers on the project timeline&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Are we designing it right?" — Before You Build
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Threat Modeling
&lt;/h3&gt;

&lt;p&gt;A structured brainstorm at design time: for each component, how would an attacker abuse it? The output is a prioritized list of threats and mitigations — before a single line of code exists, when fixes are cheapest.&lt;/p&gt;

&lt;p&gt;The frameworks you'll hear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STRIDE&lt;/strong&gt; — the classic threat checklist: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PASTA&lt;/strong&gt; — a risk-centric, seven-stage methodology that ties threats to business impact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DREAD&lt;/strong&gt; — a scoring model for ranking threats: Damage, Reproducibility, Exploitability, Affected users, Discoverability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; Microsoft Threat Modeling Tool, OWASP Threat Dragon, IriusRisk&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture / Design Review
&lt;/h3&gt;

&lt;p&gt;Security engineers walk the diagrams and data flows: where are the trust boundaries, is authentication centralized, are secrets in a vault, does a frontend talk directly to a backend when it shouldn't. If you've ever defended an architecture diagram in front of a senior reviewer, you've lived this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt; cloud well-architected security pillars, OWASP ASVS&lt;/p&gt;

&lt;h2&gt;
  
  
  "Did we build it right?" — During and After the Build
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Secure Code Review (Manual)
&lt;/h3&gt;

&lt;p&gt;A human reads the code — especially auth logic, crypto usage, and input handling — catching design-level flaws that automated SAST cannot, like "this check can be bypassed by calling the API endpoints in a different order." Tooling assists (Semgrep, CodeQL, PR review workflows), but the value is the brain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Review / Hardening Assessment
&lt;/h3&gt;

&lt;p&gt;Compare actual server, database, and cloud settings against a hardened baseline: TLS versions, open ports, default accounts, logging enabled. The gold-standard baselines are the &lt;strong&gt;CIS Benchmarks&lt;/strong&gt; and &lt;strong&gt;DISA STIGs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; CIS-CAT, Nessus compliance scans, Azure Policy&lt;/p&gt;

&lt;h2&gt;
  
  
  "Are we running it right?" — Continuously
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cloud Security Posture Assessment
&lt;/h3&gt;

&lt;p&gt;The configuration review idea, applied to your entire cloud estate, continuously: public storage, over-permissive IAM, missing encryption, exposed endpoints — all scored against benchmarks in near-real time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; Microsoft Defender for Cloud (secure score), Prisma Cloud, Wiz, Orca&lt;/p&gt;

&lt;h2&gt;
  
  
  "Can we prove it to outsiders?" — When Auditors Arrive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Security Audit / Compliance Assessment
&lt;/h3&gt;

&lt;p&gt;An external auditor checks whether you &lt;em&gt;actually follow&lt;/em&gt; a standard — through evidence, policies, interviews, and screenshots. The output is a certification or findings report, never an exploit.&lt;/p&gt;

&lt;p&gt;The standards you'll meet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ISO 27001&lt;/strong&gt; — certifies your information security management system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOC 2&lt;/strong&gt; — a trust-criteria report, the staple for SaaS vendors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PCI DSS&lt;/strong&gt; — mandatory if you touch card data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HIPAA / GDPR&lt;/strong&gt; — health data and personal data regulations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; GRC platforms like Vanta, Drata, ServiceNow GRC — but mostly document review.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Are we spending money right?" — Above Everything
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Risk Assessment
&lt;/h3&gt;

&lt;p&gt;The business-level exercise that sits on top of all of it: list assets → identify threats → score likelihood × impact → prioritize. This decides where the security budget goes and feeds every other activity on this page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frameworks:&lt;/strong&gt; ISO 27005, NIST 800-30, and &lt;strong&gt;FAIR&lt;/strong&gt; if you want risk quantified in actual currency.&lt;br&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; honestly, spreadsheets — or a GRC platform if you're fancy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Timeline Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Designing it right?&lt;/td&gt;
&lt;td&gt;Threat Modeling, Architecture Review&lt;/td&gt;
&lt;td&gt;Before build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built it right?&lt;/td&gt;
&lt;td&gt;Code Review, Config Review&lt;/td&gt;
&lt;td&gt;During/after build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running it right?&lt;/td&gt;
&lt;td&gt;Cloud Posture Assessment&lt;/td&gt;
&lt;td&gt;Continuously&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proving it?&lt;/td&gt;
&lt;td&gt;Compliance Audit&lt;/td&gt;
&lt;td&gt;Audit season&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spending right?&lt;/td&gt;
&lt;td&gt;Risk Assessment&lt;/td&gt;
&lt;td&gt;Always, above all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Remember: the testing groups &lt;strong&gt;attack&lt;/strong&gt;; this group &lt;strong&gt;inspects with full access&lt;/strong&gt;. Both are necessary — inspection finds what attacks miss, and attacks prove what inspections suspect.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Final post in the series: the cloud alphabet soup — CSPM, CWPP, CIEM, CNAPP, and EASM.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
    </item>
    <item>
      <title>From Vulnerability Scans to Red Teams: The Offensive Security Ladder</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Sat, 22 Aug 2026 04:29:14 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/from-vulnerability-scans-to-red-teams-the-offensive-security-ladder-365b</link>
      <guid>https://dev.to/vicky_acedia/from-vulnerability-scans-to-red-teams-the-offensive-security-ladder-365b</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of 4 in the "Security Testing Landscape" series&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every company eventually asks: "are we actually hackable?" There's an entire industry of ways to answer that question, and they form a &lt;strong&gt;ladder of realism and cost&lt;/strong&gt;. This post climbs it rung by rung.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rung 1: VA — Vulnerability Assessment
&lt;/h2&gt;

&lt;p&gt;An automated scanner sweeps your servers, VMs, and network for &lt;strong&gt;known&lt;/strong&gt; CVEs and misconfigurations. It's breadth over depth: it finds and lists problems, but never exploits them. Think of it as a metal detector — it beeps, it doesn't dig.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; Nessus, Qualys, OpenVAS, Rapid7 InsightVM&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; Usually grey (the scanner gets network access or credentials); can run uncredentialed as black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rung 2: PT — Penetration Testing
&lt;/h2&gt;

&lt;p&gt;Now a &lt;strong&gt;human&lt;/strong&gt; takes the scanner's findings and actually exploits them — chaining vulnerabilities, escalating privileges, reaching real data — to prove genuine business impact. Depth over breadth, scoped and time-boxed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; Burp Suite, Metasploit, Nmap, SQLMap, BloodHound, Cobalt Strike&lt;/p&gt;

&lt;p&gt;This is where the black/grey/white box terminology originates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Black box:&lt;/strong&gt; the tester gets nothing but a URL or IP — simulates an external attacker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grey box:&lt;/strong&gt; the tester gets a normal user account and basic docs — simulates an insider or a compromised user. This is the most common flavor in practice, and the best value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;White box:&lt;/strong&gt; the tester gets source code, architecture diagrams, and admin credentials — maximum coverage per paid day.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rung 3: VAPT — The Compliance Staple
&lt;/h2&gt;

&lt;p&gt;VAPT is simply &lt;strong&gt;VA + PT packaged together&lt;/strong&gt;: scan everything automatically, then let a human manually exploit the interesting bits. This is the standard annual engagement companies buy for compliance, and the term you'll see on every audit checklist in this part of the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rung 4: Red Teaming
&lt;/h2&gt;

&lt;p&gt;A red team gets a &lt;strong&gt;goal&lt;/strong&gt;, not a scope: "reach the HR database without being detected." Weeks long, stealthy, no holds barred — phishing, physical entry, custom malware, whatever works. Crucially, it tests your &lt;strong&gt;defenders and detection capability&lt;/strong&gt;, not just your systems. The blue team usually isn't told it's happening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; Cobalt Strike, Sliver, Mythic, GoPhish&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; Black by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rung 5: Purple Teaming
&lt;/h2&gt;

&lt;p&gt;Red and blue sit at the &lt;strong&gt;same table&lt;/strong&gt;. Red runs an attack technique, blue immediately checks "did our SIEM catch that?", they tune the detection, and repeat. Collaborative rather than adversarial — the fastest way to actually improve detection coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; MITRE ATT&amp;amp;CK framework, Atomic Red Team, VECTR&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; White — full transparency is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rung 6: BAS — Breach &amp;amp; Attack Simulation
&lt;/h2&gt;

&lt;p&gt;Software that &lt;strong&gt;continuously and automatically&lt;/strong&gt; replays known attack techniques inside your environment to verify your controls (EDR, SIEM, WAF) still detect them — an always-on mini red team that never sleeps or invoices per day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; AttackIQ, SafeBreach, Cymulate, Picus&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; Grey/white — agents installed inside, techniques known.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rung 7: Bug Bounty
&lt;/h2&gt;

&lt;p&gt;Open the doors and invite &lt;strong&gt;researchers worldwide&lt;/strong&gt; to attack your production app, paying per valid bug instead of per day. Continuous coverage, pay-for-results economics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platforms:&lt;/strong&gt; HackerOne, Bugcrowd, Intigriti&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; Black — researchers get exactly what the public gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Human-Focused Rungs
&lt;/h2&gt;

&lt;p&gt;Three assessments attack things other than servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Social Engineering / Phishing Simulation&lt;/strong&gt; targets your &lt;strong&gt;people&lt;/strong&gt; — fake phishing emails, phone pretexting (vishing), USB drops — measuring click and credential-submission rates. Tools: GoPhish, KnowBe4, Microsoft Attack Simulator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wireless Assessment&lt;/strong&gt; targets your &lt;strong&gt;airwaves&lt;/strong&gt; — rogue access points, WPA2 handshake cracking, evil-twin attacks. Tools: Aircrack-ng, Kismet, WiFi Pineapple.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical Security Testing&lt;/strong&gt; targets your &lt;strong&gt;building&lt;/strong&gt; — tailgating, badge cloning, lock picking, plugging into exposed ports. Tools: Proxmark, LAN Turtle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are black box: the targets don't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ladder, One Line
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;VA&lt;/strong&gt; (scanner lists bugs) → &lt;strong&gt;PT&lt;/strong&gt; (human proves bugs) → &lt;strong&gt;VAPT&lt;/strong&gt; (both — the compliance staple) → &lt;strong&gt;Red Team&lt;/strong&gt; (full attack, tests your people and detection) → &lt;strong&gt;Purple&lt;/strong&gt; (red and blue learn together) → &lt;strong&gt;BAS&lt;/strong&gt; (automate it forever) → &lt;strong&gt;Bug Bounty&lt;/strong&gt; (outsource it to the world).&lt;/p&gt;

&lt;p&gt;Each rung up costs more and simulates reality more faithfully. Most organizations start with VAPT and grow upward.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next in the series: the quiet group that never attacks anything — reviews, audits, and threat modeling.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
    </item>
    <item>
      <title>The Alphabet Soup of Application Security Testing, Explained With a Car</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Sat, 22 Aug 2026 04:28:48 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/the-alphabet-soup-of-application-security-testing-explained-with-a-car-234k</link>
      <guid>https://dev.to/vicky_acedia/the-alphabet-soup-of-application-security-testing-explained-with-a-car-234k</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1 of 4 in the "Security Testing Landscape" series&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've ever sat in a security review meeting and heard SAST, DAST, IAST, and SCA thrown around in the same sentence, you know the feeling: everyone nods, half the room is quietly Googling. This post fixes that permanently, using one analogy — &lt;strong&gt;your application is a car you're building and driving.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Four
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SAST — Static Application Security Testing
&lt;/h3&gt;

&lt;p&gt;SAST is reading the car's &lt;strong&gt;blueprint&lt;/strong&gt; before it's ever built. You spot that the brakes are drawn wrong without starting an engine.&lt;/p&gt;

&lt;p&gt;In IT terms: SAST scans your &lt;strong&gt;source code&lt;/strong&gt; for insecure patterns — SQL injection, hardcoded credentials, weak crypto — without running the application. It lives in your CI pipeline and fires on every commit or pull request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; SonarQube, Checkmarx, Fortify, Semgrep, GitHub CodeQL&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; White box — it has full access to your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  DAST — Dynamic Application Security Testing
&lt;/h3&gt;

&lt;p&gt;DAST is &lt;strong&gt;test-driving the finished car and deliberately trying to crash it&lt;/strong&gt;. It never looks at the blueprint.&lt;/p&gt;

&lt;p&gt;In IT terms: DAST attacks your &lt;strong&gt;running, deployed application&lt;/strong&gt; over HTTP exactly like an external hacker would — malicious payloads into URLs, forms, and headers. It has zero knowledge of your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; OWASP ZAP, Burp Suite, Acunetix, Invicti&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; Black box — it only sees what an attacker sees.&lt;/p&gt;

&lt;h3&gt;
  
  
  IAST — Interactive Application Security Testing
&lt;/h3&gt;

&lt;p&gt;IAST is a &lt;strong&gt;sensor inside the car during the test drive&lt;/strong&gt;. When the brakes get slammed, it feels exactly which part shook.&lt;/p&gt;

&lt;p&gt;In IT terms: an agent sits inside the running app (in the JVM or .NET runtime) and watches code execute while your QA or DAST tests run. Result: it pinpoints the exact vulnerable line, combining the strengths of SAST and DAST.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; Contrast Security, Synopsys Seeker, HCL AppScan&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; Grey box — the app is running, but the agent sees internals.&lt;/p&gt;

&lt;h3&gt;
  
  
  SCA — Software Composition Analysis
&lt;/h3&gt;

&lt;p&gt;Your build might be flawless, but the &lt;strong&gt;tires you bought from another shop have a recall notice&lt;/strong&gt;. SCA checks the parts you didn't make.&lt;/p&gt;

&lt;p&gt;In IT terms: SCA scans your dependency manifests — package.json, pom.xml, .csproj — against CVE databases for vulnerable open-source libraries (think Log4j) and license risks. Given that most modern codebases are 70–90% third-party code, this one is not optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; Snyk, Mend, OWASP Dependency-Check, GitHub Dependabot&lt;br&gt;
&lt;strong&gt;Box:&lt;/strong&gt; White box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Supporting Cast
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RASP (Runtime Application Self-Protection)&lt;/strong&gt; is the odd one out — it's not a test at all. It's the &lt;strong&gt;airbag&lt;/strong&gt;: an agent embedded in your production app that blocks real attacks as they happen, like killing a SQL injection mid-execution. Tools: Contrast Protect, Imperva RASP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MAST (Mobile AST)&lt;/strong&gt; is the same testing story for a &lt;strong&gt;motorbike&lt;/strong&gt; — SAST and DAST applied to APK/IPA binaries, hunting mobile-specific issues like insecure local storage and reverse-engineering exposure. Tools: MobSF, NowSecure, Appknox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API Security Testing&lt;/strong&gt; ignores the car body and inspects the &lt;strong&gt;fuel pipe connections&lt;/strong&gt;. Doors can be locked while a leaky pipe sinks you: it tests APIs directly for auth bypass, BOLA/IDOR, and mass assignment, usually driven by the OpenAPI spec. Tools: Postman, Burp Suite, 42Crunch, Salt Security. Typically grey box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fuzzing&lt;/strong&gt; pours &lt;strong&gt;random junk into the fuel tank&lt;/strong&gt; — sand, juice, bolts — until the engine chokes. Massive volumes of malformed input fired at an app or API until it crashes or misbehaves. Tools: AFL++, libFuzzer, Burp Intruder, RESTler. Typically black box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secret Scanning&lt;/strong&gt; checks whether someone &lt;strong&gt;left the car keys taped to the windshield&lt;/strong&gt; — leaked API keys, tokens, and connection strings in repos and git history. Tools: GitLeaks, TruffleHog, GitHub Secret Scanning. White box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container Scanning&lt;/strong&gt; inspects the &lt;strong&gt;shipping crate&lt;/strong&gt; the car arrives in — Docker images scanned layer by layer for OS-level CVEs before deployment. Tools: Trivy, Grype, Aqua. White box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IaC Scanning&lt;/strong&gt; audits the &lt;strong&gt;factory setup instructions&lt;/strong&gt;. If the manual says "leave the door open," every car built there is unsafe: Terraform, Bicep, ARM, and Kubernetes YAML scanned for misconfigurations before anything is provisioned. Tools: Checkov, tfsec, Terrascan, KICS. White box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Timeline That Makes It Stick
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before the app runs&lt;/strong&gt; (paperwork and parts): SAST, SCA, Secret Scanning, Container Scanning, IaC Scanning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;While it runs in testing&lt;/strong&gt; (test drives): DAST, IAST, Fuzzing, API Testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;While it runs in production&lt;/strong&gt; (the airbag): RASP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the box rule: anything that reads your &lt;strong&gt;files&lt;/strong&gt; is white box; anything that attacks the &lt;strong&gt;running app from outside&lt;/strong&gt; is black box; the agent that does both is grey.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next in the series: the offensive world — VA, PT, VAPT, red teams and bug bounties.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
    </item>
    <item>
      <title>The Product Maturity Matrix: How We Measure Whether Our Internal Products Are Actually "Grown Up"</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Sat, 22 Aug 2026 04:07:50 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/the-product-maturity-matrix-how-we-measure-whether-our-internal-products-are-actually-grown-up-2d6l</link>
      <guid>https://dev.to/vicky_acedia/the-product-maturity-matrix-how-we-measure-whether-our-internal-products-are-actually-grown-up-2d6l</guid>
      <description>&lt;p&gt;Every engineering team ships features. Very few teams can answer a harder question: &lt;em&gt;is this product mature?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not "does it work" — mature. Can it survive an audit? Can it survive the person who built it leaving? Can it survive an AI prompt injection attack at 2 AM on a weekend?&lt;/p&gt;

&lt;p&gt;Recently, I was asked to review and refine our product maturity matrix — the framework we use to assess every internal product across our portfolio. What started as a review exercise turned into a genuinely useful lesson in what "maturity" actually means for modern products, especially ones with AI baked in. Here's what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a maturity matrix at all?
&lt;/h2&gt;

&lt;p&gt;When you're running a portfolio of internal products — a PSA platform, an ATS, an AI assistant, dozens of integrations — you can't rely on gut feel. Each product has different teams, different tech debt, different risk profiles. A maturity matrix gives you one honest lens across all of them.&lt;/p&gt;

&lt;p&gt;The version I reviewed covered 12 areas. By the time I was done, it was 10 — and sharper for it. The final structure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Access Management&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security Monitoring&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encryption &amp;amp; Data Protection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Security &amp;amp; Risk Management&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architecture &amp;amp; Design&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Quality&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration Management&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DevOps &amp;amp; Engineering Automation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Engineering Excellence&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each area has five concrete criteria. Not aspirations — checkable statements. "Rollback procedure is defined &lt;strong&gt;and tested&lt;/strong&gt;." "Access reviews are done periodically &lt;strong&gt;with business owners&lt;/strong&gt;." The difference between a checklist and a wishlist is whether you can fail it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: Kill the duplicates, or the matrix eats itself
&lt;/h2&gt;

&lt;p&gt;The first thing I found when reviewing was overlap. "Security attacks" appeared under Observability. "AI red-teaming" appeared in two places. Credential expiry monitoring was floating around with no clear home.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds. When the same control lives in three rows, three teams either all claim it or all assume someone else owns it. Both outcomes are bad.&lt;/p&gt;

&lt;p&gt;The fix was a simple ownership rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observability = visibility.&lt;/strong&gt; Can you &lt;em&gt;see&lt;/em&gt; what's happening — logs, dashboards, traces, latency, failures?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Monitoring = detection and response.&lt;/strong&gt; Attacks, network traffic, alerts, SLAs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Security = assessment.&lt;/strong&gt; Red-teaming, risk reviews, guardrail evaluations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration Management = operational hygiene.&lt;/strong&gt; Job monitoring, credential and token expiry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One control, one home. Everything got easier after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: "100% accuracy" is not a criterion — it's a trap
&lt;/h2&gt;

&lt;p&gt;The original data quality row demanded "100% accuracy of all business reports." Sounds rigorous. It's actually the opposite.&lt;/p&gt;

&lt;p&gt;You cannot measure 100%. You cannot prove it. Every audit against it either fails or gets hand-waved — and hand-waving is precisely what a maturity matrix exists to eliminate.&lt;/p&gt;

&lt;p&gt;We replaced it with something you can actually run:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Business reports are validated periodically by the tech team against source data, and reported data issues are resolved within a defined SLA.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Measurable. Auditable. Honest. If your maturity criteria require perfection, teams will quietly stop taking the whole matrix seriously. Write criteria a good team can pass and a struggling team will visibly fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: AI changed what "mature" means
&lt;/h2&gt;

&lt;p&gt;Five years ago, a maturity matrix stopped at DevOps and monitoring. Today, if your products have AI agents in them — and ours increasingly do — you need two new muscles:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Security &amp;amp; Risk Management&lt;/strong&gt; as a first-class area. Prompt injection, data leakage, model misuse — these aren't hypotheticals. In our own security assessments we've caught things like PII slipping out through encoded payloads that a naive filter would never flag. Red-teaming your own AI features before someone else does it for you is now table stakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI observability&lt;/strong&gt; inside the observability row. Traditional APM tells you the request took 800ms. It doesn't tell you what the user asked, what the model answered, how many tokens it burned, or where in the agent chain the latency lives. Mature AI products trace the full conversation path: query → layers → response, with token usage and guardrail outcomes visible.&lt;/p&gt;

&lt;p&gt;If your maturity framework predates your AI features, it's assessing a product that no longer exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: The most embarrassing finding is the most valuable one
&lt;/h2&gt;

&lt;p&gt;One row in the matrix — Encryption &amp;amp; Data Protection — contained criteria that had clearly been copy-pasted from a completely different section. It described automation workflows. Nothing about encryption at all.&lt;/p&gt;

&lt;p&gt;Nobody had noticed, because nobody had &lt;em&gt;read&lt;/em&gt; it. It had been reviewed in meetings, circulated in emails, and nodded at repeatedly.&lt;/p&gt;

&lt;p&gt;That's the real argument for periodic deep reviews of governance documents: not to add more rows, but to check that the existing ones still say what everyone assumes they say. We rewrote it properly — encryption at rest and in transit, secrets in a managed vault with no hardcoded credentials, PII classification and masking, retention and disposal policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 5: Merge what belongs together
&lt;/h2&gt;

&lt;p&gt;The original matrix had separate rows for "Coding &amp;amp; Secure Practices" and "Release Notes," plus an empty placeholder called "Engineering Excellence." We merged all three.&lt;/p&gt;

&lt;p&gt;Code review, static analysis, dependency scanning, release notes, updated documentation — these aren't separate disciplines. They're one discipline: &lt;em&gt;the craft of shipping responsibly&lt;/em&gt;. Splitting them across rows just multiplied the paperwork without adding rigor.&lt;/p&gt;

&lt;p&gt;A shorter matrix that people actually use beats a longer one they skim.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A product maturity matrix isn't a compliance artifact. Done well, it's a mirror. It tells you which of your products would survive scrutiny and which are running on luck and tribal knowledge.&lt;/p&gt;

&lt;p&gt;Three principles carried the whole review:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every criterion must be checkable.&lt;/strong&gt; If you can't fail it, delete it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every control needs exactly one home.&lt;/strong&gt; Overlap is unowned risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The matrix must evolve with the stack.&lt;/strong&gt; AI features demand AI-shaped criteria.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your team has a maturity framework gathering dust, open it up and actually read every row. You might find automation criteria hiding in your encryption section too.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What does product maturity look like in your organization? I'd genuinely like to hear how other teams are handling AI security and observability in their governance frameworks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systems</category>
      <category>product</category>
    </item>
    <item>
      <title>Serving Millions of Users Without Melting the Database (Explained with Swiggy)</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Thu, 20 Aug 2026 04:08:33 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/serving-millions-of-users-without-melting-the-database-explained-with-swiggy-14ka</link>
      <guid>https://dev.to/vicky_acedia/serving-millions-of-users-without-melting-the-database-explained-with-swiggy-14ka</guid>
      <description>&lt;p&gt;At 8:30 PM on a Friday, millions of people open a food app at once. How does it stay fast — and how does the database survive? Not with one trick, but with five layers, each removing load before it reaches the expensive part. Here they are, using Swiggy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 5 techniques
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;One-line&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keep hot data close, skip the slow DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Load Balancing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spread traffic across many servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CDN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serve content from near the user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Partitioning / Sharding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Split one huge DB into pieces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Autoscaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Add/remove servers automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. Caching
&lt;/h2&gt;

&lt;p&gt;Keep frequently-used data somewhere fast so you don't hit the slow database every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where caches live:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Swiggy example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On your phone&lt;/td&gt;
&lt;td&gt;Your cart, cached locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CDN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Edge servers worldwide&lt;/td&gt;
&lt;td&gt;Restaurant images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Distributed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A shared cache cluster&lt;/td&gt;
&lt;td&gt;Menu, sessions (Redis)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;How writes interact with the cache:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache-aside&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App fills the cache on a miss&lt;/td&gt;
&lt;td&gt;Simple; first read is slow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Write-through&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Write to cache + DB together&lt;/td&gt;
&lt;td&gt;Always fresh; writes slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Write-back&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Write to cache now, DB later&lt;/td&gt;
&lt;td&gt;Fast writes; risk of loss if cache dies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; menu in a distributed cache (Redis), images on a CDN, cart on the client. Different data, different cache home.&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; Azure Cache for Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The golden rule:&lt;/strong&gt; cache things where "a few seconds stale is fine" (menu, restaurant list). Never cache things where stale = wrong (wallet balance, payment status).&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Load Balancing
&lt;/h2&gt;

&lt;p&gt;Spread incoming traffic across many identical servers so no single one drowns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              ┌→ Server 1
Users → [ LB ]┼→ Server 2
              └→ Server 3   (LB picks which one)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two types — L4 vs L7:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;L4 (Transport)&lt;/th&gt;
&lt;th&gt;L7 (Application)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sees&lt;/td&gt;
&lt;td&gt;IP + port only&lt;/td&gt;
&lt;td&gt;Full HTTP (URL, headers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Faster, dumber&lt;/td&gt;
&lt;td&gt;Smarter, slightly slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can do&lt;/td&gt;
&lt;td&gt;Raw distribution&lt;/td&gt;
&lt;td&gt;Route &lt;code&gt;/search&lt;/code&gt; vs &lt;code&gt;/pay&lt;/code&gt; to different pools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;How it picks a server:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Round-robin&lt;/td&gt;
&lt;td&gt;Next server in turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Least connections&lt;/td&gt;
&lt;td&gt;The least-busy server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP hash&lt;/td&gt;
&lt;td&gt;Same user → same server (sticky)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; millions of requests spread across hundreds of servers. An L7 balancer routes &lt;code&gt;/search&lt;/code&gt; to search servers and &lt;code&gt;/payment&lt;/code&gt; to payment servers.&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; Load Balancer (L4), Application Gateway / Front Door (L7).&lt;/p&gt;


&lt;h2&gt;
  
  
  3. CDN — Content Delivery Network
&lt;/h2&gt;

&lt;p&gt;Copy static content to servers around the world, so users get it from &lt;em&gt;nearby&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ No CDN:  user in Chennai → fetches image from a US server (slow, far)
✅ CDN:     user in Chennai → fetches from a Chennai edge server (fast, near)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How content actually gets there — it's pull-based, not push:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First user in a region requests &lt;code&gt;logo.png&lt;/code&gt; → the edge has nothing → it &lt;strong&gt;fetches from your origin&lt;/strong&gt;, stores a copy, serves it.&lt;/li&gt;
&lt;li&gt;Next users in that region → served from the &lt;strong&gt;edge copy&lt;/strong&gt;; origin untouched.&lt;/li&gt;
&lt;li&gt;Each edge manages its own cache, so another region still pulls from origin until &lt;em&gt;its&lt;/em&gt; edge caches it too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You never "upload" files to a CDN. The &lt;strong&gt;first request in each region fills the cache&lt;/strong&gt; — which is why it needs almost no setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three things beginners get wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Myth&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"The CDN caches automatically"&lt;/td&gt;
&lt;td&gt;On modern services (like Azure Front Door) caching is &lt;strong&gt;opt-in per route&lt;/strong&gt; — you switch it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"It's safe to cache everything"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Never cache authenticated or per-user pages&lt;/strong&gt; — a shared edge cache can serve one user's data to another&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Staleness is controlled"&lt;/td&gt;
&lt;td&gt;If your origin sends no &lt;code&gt;Cache-Control&lt;/code&gt;, the default TTL can be &lt;strong&gt;days&lt;/strong&gt; — set your own&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; restaurant photos, app JS/CSS, menu images — all static, all cached at the nearest edge. Your biryani photo loads instantly because it's cached 20km away, not 12,000km. But the &lt;em&gt;logged-in order page&lt;/em&gt; is never cached — that stays dynamic.&lt;br&gt;
&lt;strong&gt;Key:&lt;/strong&gt; a CDN is caching for &lt;em&gt;location&lt;/em&gt;. Distance = latency, so serve from close by.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Partitioning / Sharding
&lt;/h2&gt;

&lt;p&gt;Split one giant database into smaller pieces (shards), each holding part of the data.&lt;/p&gt;

&lt;p&gt;The make-or-break decision is the &lt;strong&gt;partition key&lt;/strong&gt; — &lt;em&gt;how&lt;/em&gt; you split.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Watch out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hash(key)&lt;/code&gt; decides the shard&lt;/td&gt;
&lt;td&gt;Even spread, but no range queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Range&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A–M here, N–Z there&lt;/td&gt;
&lt;td&gt;Simple, but can create hotspots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Directory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A lookup table maps key → shard&lt;/td&gt;
&lt;td&gt;Flexible, but the lookup is a dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scales beyond one machine&lt;/td&gt;
&lt;td&gt;Cross-shard queries are painful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each shard is smaller/faster&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;The partition key is ~irreversible&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; orders partitioned by &lt;code&gt;cityId&lt;/code&gt; or &lt;code&gt;userId&lt;/code&gt; — Bengaluru's orders on one shard, Delhi's on another.&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; this is exactly the &lt;strong&gt;Cosmos DB partition key&lt;/strong&gt; — the single most important, effectively permanent design choice you make up front.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;The trap:&lt;/strong&gt; a bad key creates a &lt;strong&gt;hot partition&lt;/strong&gt; — one shard takes all the traffic while the others sit idle. Choose a key that spreads load &lt;em&gt;evenly&lt;/em&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. Autoscaling
&lt;/h2&gt;

&lt;p&gt;Automatically add servers when busy, remove them when quiet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 PM (quiet):   ▪▪         (3 servers)
8:30 PM (peak): ▪▪▪▪▪▪▪▪▪▪ (auto-scaled to 20)
2 AM (dead):    ▪          (scaled back to 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reactive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scale when CPU/queue crosses a threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Predictive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scale &lt;em&gt;ahead&lt;/em&gt; of a known pattern (the dinner rush)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pay only for what you use&lt;/td&gt;
&lt;td&gt;Scaling isn't instant (there's lag)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handles spikes automatically&lt;/td&gt;
&lt;td&gt;Cold starts on new instances&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; scale up before the 8 PM rush, scale down after midnight. Never pay for 500 servers at 3 AM.&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; VM Scale Sets, AKS autoscaler, App Service autoscale.&lt;/p&gt;




&lt;h2&gt;
  
  
  How they connect
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CDN            → serves static content from the edge (never hits your servers)
Load Balancer  → spreads the dynamic traffic that remains
Caching        → cuts DB reads for that traffic
Partitioning   → splits the DB when caching isn't enough
Autoscaling    → adjusts server count to match live demand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer removes load before it reaches the expensive part: &lt;strong&gt;CDN catches the easy stuff → the load balancer spreads the rest → the cache skips the DB → partitioning shares the DB load → autoscaling flexes capacity.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The whole thing in one line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Scaling isn't one big trick — it's &lt;strong&gt;five layers peeling load away&lt;/strong&gt; before it reaches the database. Serve static from the edge, spread the rest, cache what you can, split what you can't, and flex capacity to match the crowd.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>design</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>How Services Send Messages Without Losing Them (Explained with Swiggy)</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Wed, 19 Aug 2026 04:05:51 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/how-services-send-messages-without-losing-them-explained-with-swiggy-58p8</link>
      <guid>https://dev.to/vicky_acedia/how-services-send-messages-without-losing-them-explained-with-swiggy-58p8</guid>
      <description>&lt;p&gt;Instead of calling each other directly, services often drop &lt;em&gt;messages&lt;/em&gt; and move on. It's more resilient — but it raises real questions. What if the receiver is down? What if a message gets processed twice and a customer is charged double? These 5 patterns are the answers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 5 patterns
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;One-line&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Message Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A buffer between sender and receiver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Publish-Subscribe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One message, many independent receivers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Competing Consumers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Many workers share one queue to scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Dead-Letter Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Where failing messages go to be inspected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Idempotent Consumer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Processing twice = same result as once&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. Message Queue
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A buffer that holds messages between sender and receiver. One message → one consumer.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order svc → [ ▪▪▪ QUEUE ▪▪▪ ] → Payment svc
            (holds messages safely)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sender &amp;amp; receiver decoupled&lt;/td&gt;
&lt;td&gt;Adds a component to manage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receiver can be down — the message waits&lt;/td&gt;
&lt;td&gt;Not instant (it's async)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Absorbs traffic spikes&lt;/td&gt;
&lt;td&gt;Ordering can be tricky&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; "charge this order" is dropped on a queue. If Payment is briefly down, the message waits — nothing is lost.&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; Service Bus &lt;strong&gt;Queue&lt;/strong&gt;.&lt;br&gt;
&lt;strong&gt;Key idea:&lt;/strong&gt; each message is consumed &lt;strong&gt;once&lt;/strong&gt;, by &lt;strong&gt;one&lt;/strong&gt; worker.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Publish-Subscribe (Pub-Sub)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One message published → delivered to MANY subscribers, each independently.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The difference from a queue: a queue delivers to &lt;em&gt;one&lt;/em&gt;. Pub-sub delivers a &lt;em&gt;copy to everyone&lt;/em&gt; who subscribed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Order placed" → [ TOPIC ] →→ Payment    (own copy)
                            →→ Restaurant (own copy)
                            →→ Analytics  (own copy)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add new subscribers without touching the sender&lt;/td&gt;
&lt;td&gt;Harder to trace who got what&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fully decoupled&lt;/td&gt;
&lt;td&gt;Eventual consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One event, many reactions&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; "order placed" → payment, restaurant, and analytics each get their own copy and react independently.&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; Service Bus &lt;strong&gt;Topic&lt;/strong&gt; (with subscriptions), or Event Grid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue vs Topic — lock this in:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Queue&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One message → one consumer&lt;/td&gt;
&lt;td&gt;One message → many subscribers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Assign this task"&lt;/td&gt;
&lt;td&gt;"Announce this happened"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  3. Competing Consumers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multiple workers pull from the SAME queue to process faster. They compete for messages.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             ┌→ Worker 1
[ QUEUE ] ───┼→ Worker 2   (whoever's free grabs the next message)
             └→ Worker 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scale by adding workers&lt;/td&gt;
&lt;td&gt;Message order isn't guaranteed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto load-balances&lt;/td&gt;
&lt;td&gt;Workers must be stateless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drains faster at peak&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; at dinner peak, 50,000 "charge order" messages pile up. Run 100 payment workers off one queue → it drains fast. Each message still goes to exactly one worker (no double-processing).&lt;br&gt;
&lt;strong&gt;This is how you scale a queue consumer.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Dead-Letter Queue (DLQ)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A separate queue for messages that keep failing — so one bad message can't block everything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The problem it solves is a &lt;strong&gt;poison message&lt;/strong&gt;: one malformed message that fails, retries, fails, retries… forever, jamming the queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ QUEUE ] → try process → fail → retry → fail (3x) → move to [ DLQ ]
                                                       ↑ inspect later; queue keeps flowing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One bad message can't block the line&lt;/td&gt;
&lt;td&gt;Needs monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed messages saved for inspection&lt;/td&gt;
&lt;td&gt;Manual cleanup&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; a corrupt order message fails 3 times → it's shoved into the DLQ → the main queue keeps flowing for everyone else. An engineer inspects the DLQ later.&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; Service Bus has a DLQ &lt;strong&gt;built-in&lt;/strong&gt; on every queue and subscription.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;The trap:&lt;/strong&gt; teams set up a DLQ and never watch it. Messages die silently. Always alert on DLQ depth.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. Idempotent Consumer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Processing the same message twice produces the SAME result as processing it once.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why you need it: queues can deliver a message &lt;strong&gt;twice&lt;/strong&gt; (a network hiccup, a retry). Without protection, "charge ₹450" runs twice → the customer is charged ₹900. 💥&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Message "charge order #123" arrives twice:
  ❌ Not idempotent: charge ₹450 + charge ₹450 = ₹900 (disaster)
  ✅ Idempotent:     "already processed #123?" → skip the 2nd → ₹450 ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How it works: track a &lt;strong&gt;unique message/operation ID&lt;/strong&gt;; if you've seen it, skip it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Safe against duplicate delivery&lt;/td&gt;
&lt;td&gt;Must store processed IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retries become harmless&lt;/td&gt;
&lt;td&gt;Adds a check on every message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; every payment carries an idempotency key. A duplicate "charge" message is detected and skipped. Charged once.&lt;br&gt;
&lt;strong&gt;This is non-negotiable in money flows&lt;/strong&gt; — "at-least-once delivery" means duplicates &lt;em&gt;will&lt;/em&gt; happen, so consumers &lt;em&gt;must&lt;/em&gt; be idempotent.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it all connects
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Message Queue        → the basic buffer (1 → 1)
Publish-Subscribe    → the broadcast version (1 → many)
Competing Consumers  → scale the consumer side (many workers, 1 queue)
Dead-Letter Queue    → catch the failures
Idempotent Consumer  → survive duplicate deliveries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The whole thing in one line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A queue assigns one task; a topic announces to all.&lt;/strong&gt; Then you scale it (competing consumers), protect it (dead-letter queue), and make it safe to retry (idempotent consumer). Get those right and messaging becomes the most reliable way for services to talk.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>microservices</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Keeping Data Correct Across Microservices (Explained with Swiggy)</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Tue, 18 Aug 2026 03:42:03 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/keeping-data-correct-across-microservices-explained-with-swiggy-33j8</link>
      <guid>https://dev.to/vicky_acedia/keeping-data-correct-across-microservices-explained-with-swiggy-33j8</guid>
      <description>&lt;p&gt;Here's the catch nobody warns you about: the moment you split an app into microservices, each one owns its own database — and suddenly there's no single source of truth. An order lives in one DB, its payment in another, its delivery in a third. So how do you keep everything correct? These 5 patterns are the answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 5 patterns
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;One-line&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Database-per-service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each service owns its data, period&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Saga&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manage a transaction that spans many services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CQRS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate the write model from the read model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Event Sourcing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Store &lt;em&gt;changes&lt;/em&gt;, not current state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Outbox&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reliably publish events without losing them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. Database-per-service
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Each microservice owns its own database. No other service touches it directly.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Order svc]   → Order DB     ❌ Payment svc CANNOT read Order DB directly
[Payment svc] → Payment DB      it must ASK the Order service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Services are truly independent&lt;/td&gt;
&lt;td&gt;No easy JOIN across services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change one DB freely&lt;/td&gt;
&lt;td&gt;Data is spread out, harder to keep consistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fault isolation&lt;/td&gt;
&lt;td&gt;You need the patterns below to fix consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; Order, Payment, and Delivery each own their DB. This is &lt;em&gt;why&lt;/em&gt; the next four patterns exist — it creates the consistency problem they solve.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;The golden rule:&lt;/strong&gt; never let one service reach into another's database. Always go through its API or events.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Saga — the big one
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A transaction that spans multiple services, done as a chain of steps with undo actions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Normally a database transaction is all-or-nothing. But across 4 services with 4 databases, you can't wrap one giant transaction around everything. So a Saga does it as steps — and if one fails, it &lt;strong&gt;compensates&lt;/strong&gt; (undoes) the earlier ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Place order:
  ✅ Order created
  ✅ Payment charged
  ❌ No delivery partner available!
  → COMPENSATE: refund payment → cancel order
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two flavors:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Choreography&lt;/th&gt;
&lt;th&gt;Orchestration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How&lt;/td&gt;
&lt;td&gt;Each service reacts to events, no boss&lt;/td&gt;
&lt;td&gt;One coordinator directs each step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analogy&lt;/td&gt;
&lt;td&gt;Dancers who know their cues&lt;/td&gt;
&lt;td&gt;A conductor leading an orchestra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for&lt;/td&gt;
&lt;td&gt;Simple flows&lt;/td&gt;
&lt;td&gt;Complex flows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downside&lt;/td&gt;
&lt;td&gt;Hard to trace&lt;/td&gt;
&lt;td&gt;The coordinator is a dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consistency without one giant lock&lt;/td&gt;
&lt;td&gt;Complex to design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each step can retry&lt;/td&gt;
&lt;td&gt;You must write "undo" logic for everything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; the classic "order stuck → auto-refund" flow. Payment succeeded but no rider? The Saga triggers the refund (a compensating transaction).&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; Durable Functions (orchestration) or Service Bus (choreography).&lt;/p&gt;


&lt;h2&gt;
  
  
  3. CQRS — Command Query Responsibility Segregation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Split your data model in two: one for writing, one for reading.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The name sounds fancy; the idea is simple. &lt;strong&gt;Writes and reads have different needs&lt;/strong&gt;, so stop forcing them through one model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WRITE side (Command): "Place order"     → normalized, safe, transactional
READ side  (Query):   "Show my orders"  → denormalized, fast, pre-shaped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two sides are kept in sync (often via events).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reads are blazing fast (pre-built views)&lt;/td&gt;
&lt;td&gt;Two models to maintain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale reads and writes separately&lt;/td&gt;
&lt;td&gt;Eventual consistency between them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Each side optimized for its job&lt;/td&gt;
&lt;td&gt;Overkill for simple apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; &lt;em&gt;writing&lt;/em&gt; an order needs strict validation. &lt;em&gt;Reading&lt;/em&gt; "your past orders" needs to be instant. CQRS lets the read side be a fast, pre-shaped view — no heavy JOINs at read time.&lt;/p&gt;

&lt;p&gt;⚠️ Don't reach for CQRS unless read and write needs really diverge. It adds real complexity.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Event Sourcing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Instead of storing the current state, store every &lt;em&gt;change&lt;/em&gt; as an event. Rebuild state by replaying them.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Normal:        balance = ₹500   (you only see the final number)
✅ Event Sourced: +₹1000 deposited
                  -₹300 order
                  -₹200 order
                  → replay = ₹500   (and you have the FULL history)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Complete audit trail (every change kept)&lt;/td&gt;
&lt;td&gt;Complex to build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can rebuild state anytime&lt;/td&gt;
&lt;td&gt;Replaying can get slow (needs snapshots)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Time travel" — see state at any past point&lt;/td&gt;
&lt;td&gt;Steep learning curve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; an order's life — &lt;code&gt;placed → confirmed → cooking → picked up → delivered&lt;/code&gt;. Store each as an event and you get the entire timeline for free (great for support and disputes), not just "delivered."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pairs with CQRS:&lt;/strong&gt; the events are the write side; you build fast read views from them.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Outbox Pattern
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Guarantees you never lose an event when you save data &lt;em&gt;and&lt;/em&gt; publish a message.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the hidden bug it fixes: you save an order to the DB, then publish "order placed" to the queue — but the app crashes &lt;em&gt;between&lt;/em&gt; those two steps. Order saved, event lost, payment never happens. 💥&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Risky:  save order → 💥 crash 💥 → publish event (never runs)
✅ Outbox: save order + event in ONE DB transaction
           → a separate process reads the "outbox" table → publishes reliably
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Never lose an event&lt;/td&gt;
&lt;td&gt;Extra table + a publisher process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data + event saved atomically&lt;/td&gt;
&lt;td&gt;Slight delay before publishing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; "order placed" MUST reach payment. The outbox writes the order and the event together in one transaction, so a crash can't drop it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How they fit together
&lt;/h2&gt;

&lt;p&gt;These aren't 5 random tools. They're layers solving one chain of problems created by splitting the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database-per-service   → creates the consistency problem
       ↓
Saga                   → keeps a multi-service transaction consistent
Outbox                 → makes sure the events driving it never get lost
CQRS                   → fast reads across all that split data
Event Sourcing         → full history + feeds the CQRS read side
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The whole thing in one line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Splitting the database is what makes microservices powerful &lt;strong&gt;and&lt;/strong&gt; what makes data hard. &lt;strong&gt;Saga&lt;/strong&gt; keeps multi-service actions consistent, &lt;strong&gt;Outbox&lt;/strong&gt; makes their events reliable, &lt;strong&gt;CQRS&lt;/strong&gt; makes reads fast, and &lt;strong&gt;Event Sourcing&lt;/strong&gt; keeps the full history. Reach for each only when the problem it solves is actually yours.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>design</category>
      <category>systems</category>
    </item>
    <item>
      <title>Where Do You Draw the Lines Between Services? (Explained with Swiggy)</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:56:10 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/where-do-you-draw-the-lines-between-services-explained-with-swiggy-4glk</link>
      <guid>https://dev.to/vicky_acedia/where-do-you-draw-the-lines-between-services-explained-with-swiggy-4glk</guid>
      <description>&lt;p&gt;Splitting an app into microservices sounds easy — until you try it. Draw the boundaries wrong and you get a distributed mess that's &lt;em&gt;harder&lt;/em&gt; to run than the monolith you started with. This post covers the 5 ideas that help you draw those lines well, using Swiggy.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 5 ideas
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;One-line&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DDD (bounded contexts)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Split by &lt;em&gt;business domain&lt;/em&gt;, not tech&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Strangler Fig&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Safely kill a monolith piece by piece&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Anti-Corruption Layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A translator guarding your clean model from a messy one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;BFF (Backend-for-Frontend)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A tailored backend per client (mobile vs web)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;API Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One front door for all services&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. DDD — Bounded Contexts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The core skill: split services by &lt;em&gt;business domain&lt;/em&gt;, not by technical layer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The word "order" means different things in different parts of Swiggy. DDD says: let each area own its own meaning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Wrong split (by tech):   [All Controllers] [All Databases] [All Logic]
✅ Right split (by domain): [Ordering] [Delivery] [Payments] [Restaurant]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;bounded context&lt;/strong&gt; = a boundary where a word has ONE clear meaning.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Word&lt;/th&gt;
&lt;th&gt;In Ordering&lt;/th&gt;
&lt;th&gt;In Delivery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Order"&lt;/td&gt;
&lt;td&gt;items + price + cart&lt;/td&gt;
&lt;td&gt;pickup + drop location + route&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same word, different meaning per context. Each context is a candidate microservice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Boundaries match the business&lt;/td&gt;
&lt;td&gt;Needs deep domain knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teams own a whole domain&lt;/td&gt;
&lt;td&gt;Over-splitting early = pain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; Ordering, Delivery, Payments, Restaurant Onboarding, Search — each a bounded context, each a team.&lt;br&gt;
&lt;strong&gt;The rule:&lt;/strong&gt; find where the &lt;em&gt;language&lt;/em&gt; changes → that's a boundary.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Strangler Fig
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Kill a monolith gradually — never a risky big-bang rewrite.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Named after a vine that grows around a tree and slowly replaces it. You wrap the old system, peel off one feature at a time, until the monolith is gone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1: [Monolith] ← all traffic
Step 2: [Facade] → routes SEARCH to a new service, rest to monolith
Step 3: more features moved out...
Step 4: [Monolith gone] — all services new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low risk, one piece at a time&lt;/td&gt;
&lt;td&gt;Slow, takes patience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ship continuously&lt;/td&gt;
&lt;td&gt;Two systems run in parallel for a while&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roll back easily&lt;/td&gt;
&lt;td&gt;The facade adds complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; moving from a 2014 monolith to microservices. You don't rewrite overnight; you strangle it feature by feature.&lt;br&gt;
&lt;strong&gt;Azure:&lt;/strong&gt; API Management as the facade — routes some paths to the old app, some to new services.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Anti-Corruption Layer (ACL)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A translator that stops a messy external system from polluting your clean model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you integrate with a third party (or a legacy system) whose data model is ugly, you don't let their mess leak into your code. You put a translation layer in between.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Your clean Payment model] ← ACL translates → [Messy external bank API]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your domain stays clean&lt;/td&gt;
&lt;td&gt;Extra layer to build/maintain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap the external system without touching your core&lt;/td&gt;
&lt;td&gt;Translation adds a little latency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; integrating a payment gateway or a legacy restaurant POS that returns weird formats. The ACL converts &lt;em&gt;their&lt;/em&gt; mess into &lt;em&gt;your&lt;/em&gt; clean shape — the Order service never sees the ugliness.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. BFF — Backend-for-Frontend
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A separate, tailored backend for each type of client.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A mobile app and a web dashboard need &lt;em&gt;different&lt;/em&gt; data shapes. Instead of one bloated API serving both badly, give each its own backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Mobile App] → [Mobile BFF]  ─┐
[Web App]    → [Web BFF]      ─┼→ shared microservices
[Partner App]→ [Partner BFF] ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Each client gets exactly what it needs&lt;/td&gt;
&lt;td&gt;More backends to maintain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile gets light payloads&lt;/td&gt;
&lt;td&gt;Some logic duplicated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change one client without breaking others&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Swiggy:&lt;/strong&gt; the customer app, the delivery-partner app, and the restaurant dashboard each hit their &lt;em&gt;own&lt;/em&gt; BFF — mobile gets tiny payloads, the dashboard gets rich data. Same core services underneath.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. API Gateway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One front door for all your services.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clients don't call 20 services directly. They hit ONE gateway, which routes, secures, and manages everything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clients → [ API Gateway ] → [order] [payment] [search] [delivery]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it handles in one place:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Routing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Send each request to the right service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Check tokens once, centrally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stop abuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSL termination&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Handle HTTPS here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aggregation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Combine several service calls into one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;✅ Good&lt;/th&gt;
&lt;th&gt;❌ Bad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One place for cross-cutting concerns&lt;/td&gt;
&lt;td&gt;A single point of failure (must be highly available)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clients don't know the internal layout&lt;/td&gt;
&lt;td&gt;Can become a bottleneck&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Azure:&lt;/strong&gt; this is API Management (APIM).&lt;/p&gt;




&lt;h2&gt;
  
  
  Gateway vs BFF (the confusion everyone has)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;API Gateway&lt;/th&gt;
&lt;th&gt;BFF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How many?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;One&lt;/strong&gt; for everyone&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;One per client type&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Job&lt;/td&gt;
&lt;td&gt;Route + secure + manage&lt;/td&gt;
&lt;td&gt;Shape data for a specific client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analogy&lt;/td&gt;
&lt;td&gt;The building's main reception&lt;/td&gt;
&lt;td&gt;A personal assistant per VIP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They &lt;strong&gt;stack&lt;/strong&gt;: clients → their BFF → the gateway → services. BFF shapes; gateway routes.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it all fits together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Mobile] → [Mobile BFF] ─┐
[Web]    → [Web BFF]     ─┼→ [API Gateway] → microservices split by [Bounded Contexts]
                                                    ↑
                          (Strangler Fig built these by peeling off the monolith)
                          (ACL guards each service from messy external systems)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The whole thing in one line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Draw service boundaries where the business language changes&lt;/strong&gt; (DDD), &lt;strong&gt;migrate to them safely&lt;/strong&gt; (Strangler Fig), &lt;strong&gt;shield them from messy outsiders&lt;/strong&gt; (ACL), &lt;strong&gt;give each client a tailored backend&lt;/strong&gt; (BFF), and &lt;strong&gt;put one front door in front of it all&lt;/strong&gt; (API Gateway). Get the lines right, and microservices actually pay off.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>design</category>
      <category>systems</category>
    </item>
    <item>
      <title>From Server Room to Cloud: Hard-Won Lessons from a Production Azure Migration</title>
      <dc:creator>Vignesh Athiappan</dc:creator>
      <pubDate>Mon, 17 Aug 2026 05:23:09 +0000</pubDate>
      <link>https://dev.to/vicky_acedia/from-server-room-to-cloud-hard-won-lessons-from-a-production-azure-migration-4431</link>
      <guid>https://dev.to/vicky_acedia/from-server-room-to-cloud-hard-won-lessons-from-a-production-azure-migration-4431</guid>
      <description>&lt;p&gt;&lt;em&gt;What I learned migrating two enterprise applications from on-premises infrastructure to Azure — the gotchas nobody warns you about, and the patterns that saved us.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Over the past several months, I led the end-to-end migration of two enterprise applications from on-premises Windows servers and SQL Server to Microsoft Azure. Frontends, backend APIs, an integration layer, a production database, scheduled jobs, CI/CD pipelines, DNS — the whole stack. We completed production cutover with a full smoke-test pass and zero rollback.&lt;/p&gt;

&lt;p&gt;It went well. But "went well" hides a lot of 2 AM debugging sessions. This post is everything I wish someone had told me before we started.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture (Generalized)
&lt;/h2&gt;

&lt;p&gt;The target landscape looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Edge:&lt;/strong&gt; CDN → Azure Front Door → WAF&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontends:&lt;/strong&gt; Two Node.js apps on Azure App Service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backends:&lt;/strong&gt; .NET 8 APIs on a shared PremiumV3 App Service Plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway:&lt;/strong&gt; Azure API Management — &lt;em&gt;for integration traffic only&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; Azure SQL Managed Instance (General Purpose) behind a private endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets:&lt;/strong&gt; Azure Key Vault with private endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled work:&lt;/strong&gt; SQL Agent jobs on the MI + Windows Task Scheduler tasks on a small infra VM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD:&lt;/strong&gt; Azure DevOps with a self-hosted agent inside the VNet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring:&lt;/strong&gt; Application Insights per app, shared Log Analytics workspace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing exotic. And yet almost every layer had at least one surprise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 1: Azure AD 401s Are Almost Always an Audience Mismatch
&lt;/h2&gt;

&lt;p&gt;Our most painful post-cutover bug: one API returned 401s for every authenticated request, even though the identical configuration "worked" on another API.&lt;/p&gt;

&lt;p&gt;Root cause: the JWT validation middleware was comparing the token's &lt;code&gt;aud&lt;/code&gt; claim against the configured &lt;code&gt;ClientId&lt;/code&gt; — a &lt;strong&gt;plain GUID&lt;/strong&gt; — while the tokens actually carried the audience as a &lt;strong&gt;full URI&lt;/strong&gt; (&lt;code&gt;api://&amp;lt;guid&amp;gt;&lt;/code&gt;). One app registration happened to issue GUID-form audiences; the other issued URI-form. Same code, different token shape.&lt;/p&gt;

&lt;p&gt;The fix was one line: set an explicit &lt;code&gt;Audience&lt;/code&gt; key in &lt;code&gt;appsettings.json&lt;/code&gt; instead of relying on &lt;code&gt;ClientId&lt;/code&gt; doubling as the audience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; When you see 401s in an Azure AD–protected API, decode the token first. Compare &lt;code&gt;aud&lt;/code&gt; character-for-character against what your middleware validates. Don't assume the ClientId is the audience.&lt;/p&gt;

&lt;p&gt;Related gotcha: when a &lt;strong&gt;gateway app registration&lt;/strong&gt; fronts an &lt;strong&gt;API app registration&lt;/strong&gt;, tokens must be requested with the &lt;em&gt;gateway's&lt;/em&gt; &lt;code&gt;client_id&lt;/code&gt;, not the API's. Requesting with the wrong one produces perfectly valid-looking tokens that fail validation downstream.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 2: Silent Logging Failures Are Worse Than Loud Errors
&lt;/h2&gt;

&lt;p&gt;We spent hours debugging an integration API that returned 500s with &lt;em&gt;no logs at all&lt;/em&gt;. Two separate issues had stacked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Serilog SQL sink was configured with &lt;strong&gt;unsubstituted placeholder strings&lt;/strong&gt; (think &lt;code&gt;#{ServerName}#&lt;/code&gt; left over from a deployment token that never got replaced). The sink failed to initialize — silently — and took all error reporting down with it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UseAuthorization()&lt;/code&gt; had been placed &lt;em&gt;inside&lt;/em&gt; a conditional block that only ran when auth was enabled. With auth toggled off for testing, the middleware pipeline was malformed and every request 500'd before reaching a controller.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;Always configure a &lt;strong&gt;console sink as a fallback&lt;/strong&gt;. If your primary sink dies, you need somewhere to see it die.&lt;/li&gt;
&lt;li&gt;Middleware order and placement is not optional decoration. &lt;code&gt;UseAuthorization()&lt;/code&gt; belongs unconditionally in the pipeline; if you need an "auth off" mode, use a global &lt;code&gt;AllowAnonymousFilter&lt;/code&gt; instead of restructuring the pipeline.&lt;/li&gt;
&lt;li&gt;Audit your deployment token replacement. A &lt;code&gt;#{Placeholder}#&lt;/code&gt; that survives into production config is a time bomb.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lesson 3: Key Vault References Need Version Pinning After Rotation
&lt;/h2&gt;

&lt;p&gt;We rotated the SQL admin password post-cutover, updated the secret in Key Vault, and expected App Services using Key Vault references to pick it up.&lt;/p&gt;

&lt;p&gt;They didn't — at least not promptly. &lt;strong&gt;Versionless Key Vault references cache&lt;/strong&gt;, and the refresh isn't instantaneous or predictable. During a credential rotation, "eventually consistent" is not what you want between your app and its database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; After rotating a secret, update App Service environment variables to a &lt;strong&gt;version-pinned&lt;/strong&gt; Key Vault reference. Yes, it means touching config on every rotation. It also means you know &lt;em&gt;exactly&lt;/em&gt; which secret version every app is using, which turns out to be worth a lot during an incident.&lt;/p&gt;

&lt;p&gt;Bonus: watch out for &lt;strong&gt;special characters in connection string passwords&lt;/strong&gt;. Semicolons, quotes, and braces need careful escaping — generate rotation passwords with a character set your connection string format tolerates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 4: SQL Agent Jobs Don't Just Lift-and-Shift to Managed Instance
&lt;/h2&gt;

&lt;p&gt;We had ~20 SQL Agent jobs to migrate. Managed Instance supports SQL Agent, but with constraints that broke our scripted-out jobs in three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Domain-scoped job owners&lt;/strong&gt; (&lt;code&gt;DOMAIN\user&lt;/code&gt;) don't exist on MI. Every job owner had to be replaced with a SQL login created for the purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sp_delete_job&lt;/code&gt; calls&lt;/strong&gt; embedded in the scripted output had to be stripped.&lt;/li&gt;
&lt;li&gt;Only &lt;strong&gt;T-SQL subsystem&lt;/strong&gt; job steps are fully supported — anything using CmdExec, SSIS, or PowerShell subsystems needs to be re-homed (we moved those workloads to Task Scheduler on an infra VM).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Treat Agent job migration as a &lt;em&gt;porting&lt;/em&gt; exercise, not a copy exercise. Script everything out, then clean systematically: owners, delete statements, subsystems.&lt;/p&gt;

&lt;p&gt;The Task Scheduler tasks themselves migrated cleanly via XML export/import — but every task's connection config had to change from &lt;strong&gt;Integrated Security to SQL authentication&lt;/strong&gt;, since there's no domain trust between an Azure VM and a Managed Instance out of the box.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 5: The Database Restore Itself Is the Easy Part
&lt;/h2&gt;

&lt;p&gt;The actual data move was almost anticlimactic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the source database offline&lt;/li&gt;
&lt;li&gt;Take a final full backup and &lt;strong&gt;verify it&lt;/strong&gt; (&lt;code&gt;RESTORE VERIFYONLY&lt;/code&gt; — do not skip this)&lt;/li&gt;
&lt;li&gt;Upload to blob storage with AzCopy&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RESTORE DATABASE ... FROM URL&lt;/code&gt; with a SAS credential on the MI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What people forget is everything &lt;em&gt;around&lt;/em&gt; the restore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recreate SQL users and role memberships.&lt;/strong&gt; Logins don't travel inside the database backup the way you'd hope. Have a script ready for every application login with its exact role grants (datareader, datawriter, execute, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;sp_updatestats&lt;/code&gt;&lt;/strong&gt; post-restore. First-day performance complaints often trace back to stale statistics, not the cloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify TDE.&lt;/strong&gt; Depending on how your database arrives, encryption at rest may not be enabled. Check it explicitly — &lt;code&gt;ALTER DATABASE ... SET ENCRYPTION ON&lt;/code&gt; if needed — before your security review does.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lesson 6: Draw the Traffic Flow, Then Draw It Again With Callouts
&lt;/h2&gt;

&lt;p&gt;Our architecture had one nuance that &lt;em&gt;everyone&lt;/em&gt; misread: the frontends called their backend APIs &lt;strong&gt;directly&lt;/strong&gt;, while only integration/partner traffic routed through API Management.&lt;/p&gt;

&lt;p&gt;Every new stakeholder assumed all traffic went through APIM. Every architecture review re-litigated it. Eventually I added an explicit callout panel to every diagram: &lt;em&gt;"APIM handles integration traffic only. UI traffic bypasses APIM."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; If a routing detail gets misread twice, it will be misread forever unless the diagram itself corrects the reader. Don't rely on people reading the arrows carefully — annotate the misconception directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 7: Workload Isolation on Managed Instance = Resource Governor
&lt;/h2&gt;

&lt;p&gt;Our integration workload could occasionally hammer the database hard enough to affect interactive users. On Azure SQL Database you'd have limited options; on &lt;strong&gt;Managed Instance, Resource Governor is available&lt;/strong&gt; and it's exactly the right tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dedicated resource pool with CPU and memory caps for integration workloads&lt;/li&gt;
&lt;li&gt;A workload group with lower importance and a MAXDOP limit&lt;/li&gt;
&lt;li&gt;A classifier function routing sessions by SQL login&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caution: &lt;strong&gt;keep the classifier function trivial&lt;/strong&gt;. It runs on every login, so anything slow in there becomes login latency for the entire instance.&lt;/p&gt;

&lt;p&gt;Also budget time for the General Purpose tier's &lt;strong&gt;IO envelope&lt;/strong&gt;. GP is remote-storage-backed; IO-heavy workloads that were fine on local SSDs on-prem can hit the ceiling. Query tuning, DOP limits, and alerting on IO percentage got us stable — but the honest long-term answer is often a tier or vCore bump.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lesson 8: Load Test "Failures" Deserve Forensics Before Panic
&lt;/h2&gt;

&lt;p&gt;Our first serious load test reported an &lt;strong&gt;18–22% error rate&lt;/strong&gt;. Cue alarm.&lt;/p&gt;

&lt;p&gt;Root-cause analysis showed the application was fine: the JMeter test plan was missing &lt;strong&gt;Authorization headers on roughly half its samplers&lt;/strong&gt;. The "errors" were 401s the app was correctly returning to unauthenticated requests.&lt;/p&gt;

&lt;p&gt;Similarly, an early scare about latency dissolved on inspection: &lt;strong&gt;p95 around 700ms with a flat p50 around 180ms&lt;/strong&gt; doesn't mean the system is degrading — it means occasional outliers exist. If p50 is flat under ramp, you don't have systemic pressure.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Never accept a raw error percentage from a load tool. Break errors down by status code and endpoint before concluding anything.&lt;/li&gt;
&lt;li&gt;Learn to read p50 vs p95 together. A scary p95 with a calm p50 is an outlier story, not a capacity story.&lt;/li&gt;
&lt;li&gt;How the frontend &lt;em&gt;calls&lt;/em&gt; APIs matters as much as API speed — parallel vs sequential call patterns change perceived latency dramatically.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lesson 9: DNS Cutover Is a Sequence, Not an Event
&lt;/h2&gt;

&lt;p&gt;The cutover itself was CNAME changes pointing custom domains at the Front Door endpoint. Simple. What made it low-drama:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-stage everything&lt;/strong&gt; at Front Door: custom domains validated, certs deployed, routes and WAF associations configured &lt;em&gt;before&lt;/em&gt; touching DNS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inventory every hostname&lt;/strong&gt;, not just the user-facing ones. We found integration and backend references still using an old domain suffix that had to be updated separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hunt for hardcoded server names in code.&lt;/strong&gt; We found an on-prem server name hardcoded in a repository base class. It didn't break cutover, but it was a landmine waiting for the day that server gets decommissioned. Grep your codebase for old hostnames before you celebrate.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lesson 10: The Migration Isn't Done at Cutover
&lt;/h2&gt;

&lt;p&gt;Green smoke tests on cutover day are the &lt;em&gt;start&lt;/em&gt; of the hardening phase, not the end of the project. Our post-cutover backlog looked like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Availability tests against the public URLs (through Front Door, not just direct)&lt;/li&gt;
&lt;li&gt;Action groups and full alert rule coverage — an alert without a notification target is decoration&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;PITR fire drill&lt;/strong&gt;: actually restore the database to a point in time and prove the runbook works&lt;/li&gt;
&lt;li&gt;Verifying the MI's &lt;strong&gt;public data endpoint is disabled&lt;/strong&gt; — the kind of thing everyone assumes and nobody checks&lt;/li&gt;
&lt;li&gt;Defender for Cloud, geo-filtering at the WAF, log consolidation&lt;/li&gt;
&lt;li&gt;Documented upgrade path (zone-redundant compute, geo-replica for DR)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; Write the "day 2" backlog &lt;em&gt;before&lt;/em&gt; cutover, as a gap register with severity ratings. Otherwise cutover euphoria eats it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Meta-Lessons
&lt;/h2&gt;

&lt;p&gt;If I compress the whole experience into four principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Distrust silence.&lt;/strong&gt; Silent sink failures, silent cache staleness, silent config placeholders — the worst bugs made no noise. Build fallback observability everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity is the hardest layer.&lt;/strong&gt; Compute and data moved predictably. Azure AD audiences, client IDs, SQL auth conversions, and login recreation caused more incidents than everything else combined.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root-cause before you react.&lt;/strong&gt; The load test scare, the 401 storm, the latency worry — every one of them looked like a crisis and turned out to be something narrower. The discipline of &lt;em&gt;"break it down before you escalate"&lt;/em&gt; saved days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagrams are living defenses against misunderstanding.&lt;/strong&gt; The best architecture doc isn't the most complete one — it's the one that preempts the specific misreading your audience keeps making.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Migrating to the cloud isn't a technology problem. It's a hundred small correctness problems, each of which is easy in isolation and only dangerous because they arrive together. Track them in a register, kill them one at a time, and cutover day gets boring — which is exactly what you want.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading. If you're planning a similar migration and want to compare notes, I'm happy to talk.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>onpremtocloudmigration</category>
    </item>
  </channel>
</rss>
