<?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: Prasad MK</title>
    <description>The latest articles on DEV Community by Prasad MK (@prasadmk).</description>
    <link>https://dev.to/prasadmk</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%2F3957831%2F7760c8c1-ed14-4984-9e32-58fdc9ae93b2.png</url>
      <title>DEV Community: Prasad MK</title>
      <link>https://dev.to/prasadmk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prasadmk"/>
    <language>en</language>
    <item>
      <title>What the GitHub Outage Taught Us About Authentication Retries</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Tue, 18 Aug 2026 17:25:03 +0000</pubDate>
      <link>https://dev.to/prasadmk/what-the-github-outage-taught-us-about-authentication-retries-1lbn</link>
      <guid>https://dev.to/prasadmk/what-the-github-outage-taught-us-about-authentication-retries-1lbn</guid>
      <description>&lt;p&gt;On August 17, 2026, GitHub went through a rough day. For nearly eight hours, large parts of the platform struggled. API requests, Actions, pull requests, Issues, webhooks, and Copilot all felt the impact. Error rates hit around 20% on web and API traffic. Archive and raw content downloads failed about half the time.&lt;/p&gt;

&lt;p&gt;The official status page eventually said engineers had identified a problematic component and applied fixes. Later updates mentioned something more interesting: they partially disabled authentication-token retries because those retries were making the problem worse.&lt;/p&gt;

&lt;p&gt;That detail is worth sitting with.&lt;/p&gt;

&lt;h2&gt;
  
  
  How retries turn a problem into a cascade
&lt;/h2&gt;

&lt;p&gt;Most systems retry failed requests. It is a reasonable default. When an authentication call times out or returns an error, the client tries again. Under normal conditions this improves reliability.&lt;/p&gt;

&lt;p&gt;Under stress it can do the opposite.&lt;/p&gt;

&lt;p&gt;Here is the basic loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Something degrades the authentication service.&lt;/li&gt;
&lt;li&gt;Clients start receiving failures or slow responses.&lt;/li&gt;
&lt;li&gt;Those clients retry.&lt;/li&gt;
&lt;li&gt;The extra traffic increases load on the already struggling service.&lt;/li&gt;
&lt;li&gt;More requests fail, which triggers more retries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The system enters a feedback loop. The retries that were meant to help become the main source of pressure. Engineers at GitHub observed this happening and chose to limit the retries so the authentication layer could recover.&lt;/p&gt;

&lt;p&gt;This pattern is sometimes called a retry storm or a metastable failure. Once the system is in that state, removing the original trigger is not always enough. The amplified traffic keeps the service saturated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Different approaches to the same problem
&lt;/h2&gt;

&lt;p&gt;Microsoft and Google have spent years designing authentication systems that try to avoid this exact trap.&lt;/p&gt;

&lt;p&gt;Microsoft Entra ID includes a backup authentication service that runs on separate infrastructure. When the primary system is degraded, the backup can continue serving tokens for existing sessions. Their client libraries (MSAL) are also written to respect rate limits and avoid aggressive retry loops. Continuous Access Evaluation lets them issue longer-lived tokens while still supporting near real-time revocation.&lt;/p&gt;

&lt;p&gt;Google tends to favor short-lived access tokens paired with disciplined client behavior. Official client libraries use exponential backoff with jitter and only retry certain classes of transient errors. Permanent failures are not retried endlessly. Token validation often relies on cached public keys, which reduces live dependency on the identity service.&lt;/p&gt;

&lt;p&gt;These systems were built with planetary scale and known failure modes in mind. Authentication is treated as critical infrastructure that needs its own isolation and fallback paths.&lt;/p&gt;

&lt;p&gt;GitHub's architecture has historically been more tightly coupled. When authentication slows down, many other services feel it quickly. The recent outage made that coupling visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why GitHub does not simply switch to Microsoft's identity stack
&lt;/h2&gt;

&lt;p&gt;GitHub is owned by Microsoft, so the question comes up often: why not just use Entra ID and MSAL?&lt;/p&gt;

&lt;p&gt;There are practical reasons.&lt;/p&gt;

&lt;p&gt;GitHub was kept relatively independent after the acquisition. That independence mattered to the developer community. Moving the entire identity system onto Entra ID would have changed the product in ways that go beyond technical convenience.&lt;/p&gt;

&lt;p&gt;The user models are also different. GitHub supports personal accounts, open source projects, and enterprises. Entra ID is optimized for organizational identity. Enterprises can already choose tighter integration through SAML, SCIM, and Enterprise Managed Users. Making that the only path would break the model that most individual developers use.&lt;/p&gt;

&lt;p&gt;MSAL itself is a client library. It helps applications acquire tokens from Entra ID. It does not replace the need for GitHub to issue, validate, and manage its own tokens for Git operations, GitHub Apps, Actions, and the API. Solving the retry cascade requires changes inside GitHub's own authentication services, not just a different client library.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this leaves us with
&lt;/h2&gt;

&lt;p&gt;The outage was disruptive. It also made a common systems problem concrete. Retries are useful until they are not. When the thing you are retrying is the authentication layer that everything else depends on, the cost of those retries can rise quickly.&lt;/p&gt;

&lt;p&gt;Good authentication systems plan for this. They limit amplification, provide fallback paths, and treat token services as critical infrastructure rather than just another internal dependency.&lt;/p&gt;

&lt;p&gt;GitHub will likely continue improving its own stack. The rest of us can take the simpler lesson: when designing retry logic, ask what happens when the dependency is already under pressure. The answer is often more important than the happy path.&lt;/p&gt;

</description>
      <category>github</category>
      <category>sre</category>
      <category>monitoring</category>
      <category>security</category>
    </item>
    <item>
      <title>The Pizza-Builder Pattern: Stop Guessing, Start Building a Menu</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Sun, 16 Aug 2026 23:08:10 +0000</pubDate>
      <link>https://dev.to/prasadmk/the-pizza-builder-pattern-stop-guessing-start-building-a-menu-5504</link>
      <guid>https://dev.to/prasadmk/the-pizza-builder-pattern-stop-guessing-start-building-a-menu-5504</guid>
      <description>&lt;p&gt;Ask an AI to "write a novel" and you get one of two outcomes, and both annoy me.&lt;/p&gt;

&lt;p&gt;It guesses. Genre, protagonist, tone, all invented on the spot. You read the output, realize it missed, and burn a full generation cycle starting over.&lt;/p&gt;

&lt;p&gt;Or it interrogates you. One clarifying question at a time, four turns deep, until you've typed more than the request would've taken to write yourself.&lt;/p&gt;

&lt;p&gt;I got tired of both, so I built a Claude skill called pizza-builder. One screen, one menu, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a pizza counter
&lt;/h2&gt;

&lt;p&gt;Build-your-own works for a reason most people never stop to think about. Someone already figured out which decisions matter (base, sauce, cheese, toppings) and narrowed each one to a short list of good options.&lt;/p&gt;

&lt;p&gt;You don't invent a pizza from nothing. You compose one from a menu somebody designed well.&lt;/p&gt;

&lt;p&gt;I applied that same logic to AI requests. Turns out the fix isn't a better prompt. It's a better structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that make the menu work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Find the real decision points, not generic categories.&lt;/strong&gt;&lt;br&gt;
Weak: "Genre: Fantasy / Sci-fi / Mystery / Other"&lt;br&gt;
Sharp: "Cozy small-town mystery / Hard sci-fi with a ticking clock / Literary fiction about a family secret / Epic fantasy with a political core"&lt;/p&gt;

&lt;p&gt;Specific options let you react instantly. Generic labels just restate the question back at you, and nobody's ever been helped by that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Match the question to its actual shape.&lt;/strong&gt; Not every decision is "pick one of four." I count seven shapes that actually show up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Toggle&lt;/strong&gt; for a plain yes/no fact, like whether this needs to survive a reboot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Radio&lt;/strong&gt; for a short list where picking one rules out the rest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropdown&lt;/strong&gt; for six or more options, so the screen stays scannable instead of a wall of buttons&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip option&lt;/strong&gt; for anything an expert might actively argue against, like not bothering with a cache&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slider&lt;/strong&gt; for a spectrum, "70% tomato, 30% pesto," instead of a fake list pretending to be a continuum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkbox&lt;/strong&gt; for anything genuinely additive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free text&lt;/strong&gt; for a number, name, or path, because forcing that into buttons just looks tidy and helps nobody&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Give every option a real "you decide."&lt;/strong&gt; Not a shrug. A stated pick, with a reason, the way a good server tells you "I'll bring you the fish, it's better tonight" instead of handing the decision right back to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Synthesize, don't summarize.&lt;/strong&gt; Four specific picks need to produce something that couldn't exist without all four combined. If the output just mentions each keyword once, the synthesis failed, not the menu.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure that taught me the hard rule
&lt;/h2&gt;

&lt;p&gt;A sunset-image picker I built once generated a prompt that said "no horizon element" in one line and "bands from horizon to zenith" two lines later. Code ran fine. Buttons worked. The image came out as flat color stripes, because the prompt itself was fighting with itself and nobody caught it.&lt;/p&gt;

&lt;p&gt;Same picker, different bug: it generated this line and shipped it straight to the user. "Can you write me a detailed, ready-to-use prompt for this, and flag anything I should reconsider?"&lt;/p&gt;

&lt;p&gt;Every automated check passed. Syntax clean, buttons functional, nothing throwing errors. And the output was a question thrown back at the person who'd already answered five questions to get there.&lt;/p&gt;

&lt;p&gt;That one stung more than the horizon bug, honestly, because it revealed something worse: passing tests tells you the code runs, not that the content does anything. So now every generated prompt gets read against three failure shapes before it ships, not just executed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing anchor&lt;/strong&gt;: nothing stated that makes the subject actually recognizable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-contradiction&lt;/strong&gt;: one line quietly undercutting another&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Punt-back&lt;/strong&gt;: a question aimed at the reader instead of an answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mechanical test for the last one is almost embarrassingly simple: does the output contain "can you," "write me," or a question mark pointed at the user? If so, it's asking for the work instead of doing it, and I'd rather catch that before someone else does.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use this, and when not to
&lt;/h2&gt;

&lt;p&gt;Route on structure, never on topic. I learned this the hard way, too. Any list of "domains this applies to" is guaranteed to miss the next domain, so I stopped keeping one.&lt;/p&gt;

&lt;p&gt;Use the menu when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's no single obvious interpretation&lt;/li&gt;
&lt;li&gt;A wrong guess costs a real redo: a full draft, a rendered image, a deck&lt;/li&gt;
&lt;li&gt;The request has three or more independent decisions, not just one missing detail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skip it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's a clear default (a headline: just write two or three and move on)&lt;/li&gt;
&lt;li&gt;The task is cheap to redo (a tweet, a rename)&lt;/li&gt;
&lt;li&gt;You want an actual opinion, not a menu dressed up as one ("Python or JavaScript?")&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get the skill
&lt;/h2&gt;

&lt;p&gt;The full SKILL.md lives here: &lt;a href="https://github.com/prasad-m-k/claude-skills/tree/main/pizza-builder" rel="noopener noreferrer"&gt;https://github.com/prasad-m-k/claude-skills/tree/main/pizza-builder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude&lt;/strong&gt;: Add it as a custom skill. Download the file, upload it in Claude's skill settings (or drop it into your skills folder on Claude Code), and it triggers on its own when a request fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT&lt;/strong&gt;: No native skill system, but the pattern still works fine. Create a Custom GPT or Project and paste the SKILL.md content into its instructions field. It'll follow the same dimension-and-shape logic when it builds a clarification screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini (Gems)&lt;/strong&gt;: Same move. Create a Gem, paste the file into its instructions, and it runs the same rules: find the real decision points, classify their shape, build the menu fresh every time.&lt;/p&gt;

&lt;p&gt;None of these platforms need the exact file structure. The value sits in the rules, not the format, so paste it in wherever the model keeps its persistent instructions and it holds.&lt;/p&gt;

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

&lt;p&gt;Early versions of this skill shipped a fixed template with pre-built dimension renderers. It only covered three of the seven shapes. Every new request had to get checked against that file's limits, and that check kept getting skipped, because who remembers to check a limit they don't know exists.&lt;/p&gt;

&lt;p&gt;I didn't fix it with a bigger template. I deleted the template. Every menu gets built fresh now, against whatever the current rules say, every single time. Nothing cached, so nothing drifts out of sync while I'm not looking.&lt;/p&gt;

&lt;p&gt;If I take one thing away from building this, it's that a good clarification system isn't a form with extra fields bolted on. It's a habit of figuring out, fresh, every time, which decisions actually matter and how each one deserves to be asked.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>promptengineering</category>
      <category>ux</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Testing Kubernetes NetworkPolicies Before They Hit Production</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Thu, 06 Aug 2026 16:36:14 +0000</pubDate>
      <link>https://dev.to/prasadmk/testing-kubernetes-networkpolicies-before-they-hit-production-1hpi</link>
      <guid>https://dev.to/prasadmk/testing-kubernetes-networkpolicies-before-they-hit-production-1hpi</guid>
      <description>&lt;p&gt;You write a NetworkPolicy, apply it to a namespace, and move on. Nobody tests it. Nobody validates it against the actual traffic the service depends on. It sits there until a deploy breaks something, and the first sign of trouble is a pod that can't reach its database.&lt;/p&gt;

&lt;p&gt;Most teams treat NetworkPolicies like documentation instead of code. Someone writes one, a reviewer skims the YAML, it gets merged, and it goes straight onto the cluster. There's no equivalent of a unit test for "can pod A talk to pod B on port 5432." That gap is where the 2am pages come from.&lt;/p&gt;

&lt;p&gt;Here are three tools that close it, plus a CI setup that catches a bad rule before merge instead of during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why NetworkPolicy bugs are hard to catch by reading YAML
&lt;/h2&gt;

&lt;p&gt;A NetworkPolicy looks simple. Selectors, ports, ingress and egress rules. But three things make them easy to get wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Default-deny is implicit.&lt;/strong&gt; The moment you add any NetworkPolicy that selects a pod, all traffic not explicitly allowed is blocked. Add a policy meant to restrict one path, and you silently cut off three others that had no policy governing them before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selectors don't validate against real labels.&lt;/strong&gt; A typo in &lt;code&gt;app: payment-svc&lt;/code&gt; versus the pod's actual &lt;code&gt;app: payment-service&lt;/code&gt; produces a policy that matches nothing. &lt;code&gt;kubectl apply&lt;/code&gt; succeeds. The policy does nothing. You find out when traffic that should be blocked isn't, or worse, traffic that should be allowed is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple policies combine additively, and nobody traces the union.&lt;/strong&gt; If three teams each own a policy touching the same namespace, the effective ruleset is the union of all three. Reading one YAML file tells you nothing about the combined effect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this shows up in a code review unless the reviewer mentally simulates traffic flow across every policy in the namespace. That doesn't scale past a handful of services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool 1: kubectl-np-viewer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kubectl-np-viewer&lt;/code&gt; is a kubectl plugin that renders the &lt;em&gt;effective&lt;/em&gt; policy for a pod or namespace, not just the raw YAML. Instead of asking you to trace intersections across five NetworkPolicy objects by hand, it computes the resolved ingress and egress rules and prints them as a readable table.&lt;/p&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl krew &lt;span class="nb"&gt;install &lt;/span&gt;np-viewer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against a namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl np-viewer &lt;span class="nt"&gt;-n&lt;/span&gt; checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output shows, per pod selector, which ingress sources and egress destinations are actually permitted after all policies in the namespace are combined. This is the tool you reach for when someone asks "can the checkout service reach Redis" and nobody wants to trace four YAML files to find out.&lt;/p&gt;

&lt;p&gt;Use it locally before you open a PR. If the effective policy doesn't match what you intended, fix the policy before it's even committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool 2: netpol-analyzer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;netpol-analyzer&lt;/code&gt; (from the np-guard project) goes further. It's built for static analysis and CI, not just interactive inspection. It can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Diff two policy sets and show exactly what connectivity changed&lt;/li&gt;
&lt;li&gt;Detect policies that have no effect (selector matches zero pods)&lt;/li&gt;
&lt;li&gt;Detect redundant rules already covered by a broader rule&lt;/li&gt;
&lt;li&gt;Generate a full connectivity map for a namespace or cluster snapshot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The diff mode is the one that matters most for CI. Given the policies currently in the cluster (or a manifest directory) and the policies in your PR branch, it tells you the delta in allowed connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netpol-analyzer diff &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir1&lt;/span&gt; manifests/main &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dir2&lt;/span&gt; manifests/pr-branch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sample result looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Changed connections between main and pr-branch:
Denied connection: checkout-service -&amp;gt; inventory-db:5432 (was allowed, now denied)
Added connection: checkout-service -&amp;gt; new-cache:6379 (newly allowed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That first line is the bug that takes down a service on merge, every time. Someone tightens egress for one workload and doesn't notice it also strips access to the database three lines down. A reviewer scanning YAML won't catch it. &lt;code&gt;netpol-analyzer diff&lt;/code&gt; puts it right at the top of the output, which is the whole point of running it.&lt;/p&gt;

&lt;p&gt;Run this as a required check on any PR that touches &lt;code&gt;networkpolicy&lt;/code&gt; manifests. Treat a "Denied connection" line touching a live dependency as a blocking failure, not a warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool 3: Cilium connectivity test
&lt;/h2&gt;

&lt;p&gt;If your cluster runs Cilium as the CNI, &lt;code&gt;cilium connectivity test&lt;/code&gt; gives you something the first two tools don't: live traffic verification against a real cluster, not just static analysis of YAML. It deploys a set of test pods, generates traffic between them under different policy conditions, and reports pass/fail based on actual packet delivery.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cilium connectivity &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--test-namespace&lt;/span&gt; cilium-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches things static analysis can't, like a CNI-level misconfiguration, an eBPF program that didn't reload correctly after a policy update, or a policy that's syntactically fine but doesn't behave as expected once the dataplane applies it. Static tools tell you what the policy &lt;em&gt;should&lt;/em&gt; do. This tool tells you what actually happens on the wire.&lt;/p&gt;

&lt;p&gt;You don't want to run the full Cilium test suite on every PR. It needs a live cluster and takes minutes, not seconds. Reserve it for a staging environment, run on merge to main or on a schedule, not on every commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building this into a CI pipeline
&lt;/h2&gt;

&lt;p&gt;The goal is a pipeline where a NetworkPolicy change gets validated automatically, and a rule that breaks an existing dependency fails the build before a team member has to notice. Here's a structure that works with GitHub Actions, adjust for GitLab CI or Jenkins as needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1: Static validation on every PR touching NetworkPolicy manifests&lt;/strong&gt;&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;netpol-validation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&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="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;manifests/**/networkpolicy*.yaml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;manifests/**/networkpolicy*.yml'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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="s"&gt;Install netpol-analyzer&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -sLo netpol-analyzer https://github.com/np-guard/netpol-analyzer/releases/latest/download/netpol-analyzer-linux-amd64&lt;/span&gt;
          &lt;span class="s"&gt;chmod +x netpol-analyzer&lt;/span&gt;
          &lt;span class="s"&gt;sudo mv netpol-analyzer /usr/local/bin/&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="s"&gt;Checkout base branch manifests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git worktree add /tmp/base origin/main -- manifests&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="s"&gt;Diff connectivity&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;netpol-analyzer diff \&lt;/span&gt;
            &lt;span class="s"&gt;--dir1 /tmp/base/manifests \&lt;/span&gt;
            &lt;span class="s"&gt;--dir2 manifests \&lt;/span&gt;
            &lt;span class="s"&gt;--output text | tee netpol-diff.txt&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="s"&gt;Fail on denied connections to known dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if grep -q "Denied connection" netpol-diff.txt; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::error::NetworkPolicy change removes existing connectivity. Review netpol-diff.txt."&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&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="s"&gt;Check for ineffective policies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;netpol-analyzer eval --dir manifests --output text --fail-on-empty-selectors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This job runs on every PR that touches a policy file. It fails the build if the diff shows a connection being removed. That's the check that would have caught the checkout-to-inventory-db example above, before merge instead of during an incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2: Live connectivity test on merge to main&lt;/strong&gt;&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;netpol-live-validation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&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="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="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;manifests/**/networkpolicy*.yaml'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;live-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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="s"&gt;Set up cluster access&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.STAGING_KUBECONFIG }}" &amp;gt; kubeconfig&lt;/span&gt;
          &lt;span class="s"&gt;echo "KUBECONFIG=$(pwd)/kubeconfig" &amp;gt;&amp;gt; $GITHUB_ENV&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="s"&gt;Apply policies to staging&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl apply -f manifests/ -n staging&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="s"&gt;Install cilium CLI&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz&lt;/span&gt;
          &lt;span class="s"&gt;tar xzf cilium-linux-amd64.tar.gz&lt;/span&gt;
          &lt;span class="s"&gt;sudo mv cilium /usr/local/bin/&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="s"&gt;Run connectivity test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cilium connectivity test --test-namespace cilium-test --timeout 10m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This second stage runs only on merge, against a real staging cluster, and catches dataplane-level problems that static diffing can't see. It's slower and it needs a live cluster, so it doesn't belong on every PR, but it's the last line of defense before the same policies get promoted to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3 (optional): scheduled drift check&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Policies drift from what's in git if anyone applies changes directly to the cluster. A nightly job that diffs the live cluster state against the manifests in main catches that:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;netpol-analyzer diff&lt;/code&gt; between a live export of the cluster's policies (&lt;code&gt;kubectl get networkpolicy -A -o yaml&lt;/code&gt;) and the manifests in git. Any difference means someone bypassed the pipeline, and you want to know before it causes a support ticket six weeks later when nobody remembers making the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this catches in practice
&lt;/h2&gt;

&lt;p&gt;A rule of thumb for interpreting output from &lt;code&gt;netpol-analyzer diff&lt;/code&gt;: a removed connection touching anything with active traffic is a merge blocker. A newly allowed connection is worth a second look but rarely urgent. An empty-selector warning means someone wrote a policy that does nothing, which is a silent no-op rather than a security control, and it should get fixed even though it won't break anything today.&lt;/p&gt;

&lt;p&gt;None of this replaces code review. It replaces the part of code review that asks us to mentally trace traffic across a namespace, which people are genuinely bad at and a diff tool is genuinely good at. Static check on every PR, live test on merge, drift check on a schedule. Do that and the first time you hear about a bad NetworkPolicy rule stops being an incident channel at 2am.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Object-Oriented Abstraction Didn't Disappear. It Moved Into Prompts</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Thu, 06 Aug 2026 06:10:56 +0000</pubDate>
      <link>https://dev.to/prasadmk/object-oriented-abstraction-didnt-disappear-it-moved-into-prompts-3lkd</link>
      <guid>https://dev.to/prasadmk/object-oriented-abstraction-didnt-disappear-it-moved-into-prompts-3lkd</guid>
      <description>&lt;p&gt;You call &lt;code&gt;car.start()&lt;/code&gt; and you don't care if it's a V8 or an electric motor underneath. That's abstraction: a stable interface, a swappable implementation.&lt;/p&gt;

&lt;p&gt;Prompt engineering usually skips that. Most prompts are one long string doing the job of a method signature, a method body, and a config file at once. Swap the model and the string that worked yesterday quietly stops working today. No error, no warning, just a slightly wrong answer.&lt;/p&gt;

&lt;p&gt;DSPy, a framework out of Stanford, applies OOP-style structure to this problem. Here's how it works.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Separate the contract from the wording
&lt;/h3&gt;

&lt;p&gt;In DSPy you don't write a prompt. You write a &lt;strong&gt;Signature&lt;/strong&gt;, an input/output contract with zero wording attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Signature&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Answer questions based on the given context.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the interface. Given context and a question, produce an answer. It reads like a method signature: &lt;code&gt;answer(context, question) -&amp;gt; answer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then you attach a strategy that satisfies it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ChainOfThought&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GenerateAnswer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ChainOfThought&lt;/code&gt; reasons before answering. Swap it for &lt;code&gt;Predict&lt;/code&gt; and you get a direct answer, no visible reasoning, same signature. The caller never notices. This is the same move as swapping a &lt;code&gt;PostgresRepository&lt;/code&gt; for a &lt;code&gt;MySQLRepository&lt;/code&gt; behind one &lt;code&gt;Repository&lt;/code&gt; interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compiling a prompt instead of writing one
&lt;/h3&gt;

&lt;p&gt;This is where DSPy earns the comparison to a compiler. Give it labeled examples and a metric (did the answer match what you expected?) and it searches over prompt phrasings and few-shot sets, scores each one, keeps the best. You don't hand-write the final prompt. DSPy compiles it.&lt;/p&gt;

&lt;p&gt;Say you tuned a prompt against GPT-4 and it performs well. Move to Claude, and that tuned wording often performs worse, because different models respond to different phrasing. Normally you retune by hand, from scratch. With DSPy, the signature stays fixed. You recompile against the new model, and the optimizer rebuilds a prompt suited to it. The contract survives the swap. Only the implementation gets rebuilt.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  A worked example
&lt;/h3&gt;

&lt;p&gt;Say your signature is &lt;code&gt;summarize(document) -&amp;gt; summary&lt;/code&gt;, and your metric checks whether the summary mentions every named entity in the source document.&lt;/p&gt;

&lt;p&gt;Run the optimizer with twenty labeled documents. It might try a zero-shot instruction first, score it, then try adding two few-shot examples, score that, then try a chain-of-thought variant that lists entities before summarizing. Whichever version scores highest on entity coverage becomes the compiled prompt. You never touched the wording. You touched the metric and the examples.&lt;/p&gt;

&lt;p&gt;Switch the underlying model from GPT-4 to Claude next month, and you rerun the same compile step against the same twenty examples and the same metric. The signature &lt;code&gt;summarize(document) -&amp;gt; summary&lt;/code&gt; never changes. Only the recipe underneath does.&lt;/p&gt;

&lt;h3&gt;
  
  
  The full mapping
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object-oriented concept&lt;/th&gt;
&lt;th&gt;DSPy equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Interface / abstract method&lt;/td&gt;
&lt;td&gt;Signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concrete class&lt;/td&gt;
&lt;td&gt;Module (&lt;code&gt;ChainOfThought&lt;/code&gt;, &lt;code&gt;ReAct&lt;/code&gt;, &lt;code&gt;ProgramOfThought&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Constructor arguments&lt;/td&gt;
&lt;td&gt;InputFields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return type&lt;/td&gt;
&lt;td&gt;OutputField&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiler&lt;/td&gt;
&lt;td&gt;Optimizer (&lt;code&gt;BootstrapFewShot&lt;/code&gt;, &lt;code&gt;MIPRO&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Polymorphism&lt;/td&gt;
&lt;td&gt;Swapping modules under one signature&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you've spent time in Spring Boot or any typed backend, this table should feel familiar on sight. A signature is a method interface. A module is one implementation of it. The optimizer is what used to be a person doing prompt engineering by trial and error, now automated and measurable.&lt;/p&gt;

&lt;h3&gt;
  
  
  What next
&lt;/h3&gt;

&lt;p&gt;Start with one signature for a task you already do by hand, question answering over a document, classification, extraction. Write the input and output fields with no prompt text at all. Attach &lt;code&gt;dspy.Predict&lt;/code&gt; first, then &lt;code&gt;dspy.ChainOfThought&lt;/code&gt;, and compare. Add ten labeled examples and a simple metric, then run &lt;code&gt;BootstrapFewShot&lt;/code&gt; and look at what it generates. That's the fastest way to see the interface-and-implementation split in practice rather than on paper.&lt;/p&gt;

&lt;p&gt;DSPy does need a training set and a metric to compile against, so it fits tasks with a checkable answer better than fully open-ended generation. Worth knowing going in, not a reason to avoid it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Prasad MK writes on distributed systems, API governance, and the architecture underneath modern AI tooling.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>architecture</category>
    </item>
    <item>
      <title>When Caches Lie: The Hidden Invalidation Traps in a Multi-Instance Spring Boot Microservice</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Sun, 02 Aug 2026 16:46:07 +0000</pubDate>
      <link>https://dev.to/prasadmk/when-caches-lie-the-hidden-invalidation-traps-in-a-multi-instance-spring-boot-microservice-44pl</link>
      <guid>https://dev.to/prasadmk/when-caches-lie-the-hidden-invalidation-traps-in-a-multi-instance-spring-boot-microservice-44pl</guid>
      <description>&lt;p&gt;Your local dev environment lied to you. It ran one instance, one JVM, one cache. Everything invalidated cleanly because there was nowhere else for stale data to hide.&lt;/p&gt;

&lt;p&gt;Then you shipped two instances behind a load balancer, and the cache started telling a different story to different users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The greenfield illusion
&lt;/h2&gt;

&lt;p&gt;Here's the setup almost everyone starts with: a Spring Boot app, &lt;code&gt;@EnableCaching&lt;/code&gt;. In local dev environment, with one pod, it works perfectly. Every write is immediately visible on the next read, because there is exactly one place for that data to live.&lt;/p&gt;

&lt;p&gt;Scale to two instances behind an ingress router and the illusion breaks. A &lt;code&gt;PUT&lt;/code&gt; lands on Instance A. A &lt;code&gt;GET&lt;/code&gt; five seconds later lands on Instance B, because that's how load balancing works, and Instance B never saw the write. It's still holding whatever it cached ten minutes ago.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fws8njrhvyyh3jvx9m47r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fws8njrhvyyh3jvx9m47r.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;diagram 1: Two JVM instances with isolated local caches pointing at one shared database&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is not a bug. It's the correct behavior of two independent JVMs with no shared memory, doing exactly what you told them to do. The DB is the only thing in this picture that's telling the truth, and neither cache is listening to it in real time.&lt;/p&gt;

&lt;p&gt;Most tutorials stop here and tell you to swap in Redis. That's necessary. It's not sufficient. Redis fixes the "two separate memory spaces" problem. It does nothing for the four scenarios below, and those are the ones that actually take down production.&lt;/p&gt;
&lt;h2&gt;
  
  
  Scenario 1: the invalidation call that quietly fails
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@CacheEvict&lt;/code&gt; looks like a guarantee. It is not. It's a method call, and method calls fail.&lt;/p&gt;

&lt;p&gt;Picture the sequence: your service commits a database transaction, then calls out to evict the cache entry. If that eviction call hits a network blip, a Redis connection pool exhaustion, or a two-second timeout, the eviction silently doesn't happen. The transaction already committed. Nothing rolls back. Nothing throws past your controller. Your monitoring dashboard shows 200 OK on the write endpoint, because from the caller's perspective, the write succeeded. It did. The cache just never found out.&lt;/p&gt;

&lt;p&gt;Now the cache holds a value that will never self-correct until its TTL expires, which might be six hours from now if you set a long TTL to reduce database load (a reasonable thing to want, for the wrong reason).&lt;/p&gt;

&lt;p&gt;A few concrete patterns that catch this instead of hoping it doesn't happen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set a TTL backstop even on entries you explicitly evict.&lt;/strong&gt; If your write frequency for a given entity is roughly once every ten minutes, a TTL of two to three times that (20 to 30 minutes) means a dropped eviction call self-heals within half an hour instead of persisting until the next deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move the evict call after commit, not before, and make it retry.&lt;/strong&gt; Spring's default behavior with &lt;code&gt;@CacheEvict(beforeInvocation=false)&lt;/code&gt; already fires after the method body runs, but "after the method body runs" is not the same as "after the transaction commits" if you're inside a &lt;code&gt;@Transactional&lt;/code&gt; boundary with deferred commit. Use &lt;code&gt;TransactionSynchronizationManager.registerSynchronization&lt;/code&gt; to fire the evict in &lt;code&gt;afterCommit&lt;/code&gt;, and wrap it in a retry with backoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderStatusService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;CacheManager&lt;/span&gt; &lt;span class="n"&gt;cacheManager&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;updateStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newStatus&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TransactionSynchronizationManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isActualTransactionActive&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;TransactionSynchronizationManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerSynchronization&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TransactionSynchronization&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="nd"&gt;@Override&lt;/span&gt;
                    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;afterCommit&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;evictWithRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// no transaction context to hook, evict right away&lt;/span&gt;
            &lt;span class="n"&gt;evictWithRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;evictWithRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attemptsLeft&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;cacheManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderStatus"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;evict&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attemptsLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;evictWithRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attemptsLeft&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// log loudly here, this entry is now relying on TTL alone&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cache evict failed after retries for order {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retry is not elegant. It's honest about the fact that eviction is a network call, and network calls fail in ways your happy-path tests will never exercise.&lt;/p&gt;

&lt;p&gt;Note the guard around &lt;code&gt;registerSynchronization&lt;/code&gt;. It only works when a transaction is actually in flight. Call &lt;code&gt;updateStatus&lt;/code&gt; from a path that isn't wrapped in &lt;code&gt;@Transactional&lt;/code&gt;, a batch job, a test, a refactor that strips the annotation, and &lt;code&gt;registerSynchronization&lt;/code&gt; throws &lt;code&gt;IllegalStateException: Transaction synchronization is not active&lt;/code&gt; instead of quietly doing nothing. Checking &lt;code&gt;isActualTransactionActive()&lt;/code&gt; first means the method still evicts correctly outside a transaction, it just does it immediately instead of waiting for a commit that will never come.&lt;/p&gt;

&lt;p&gt;There's a second version of this failure that has nothing to do with the network. &lt;code&gt;@Transactional&lt;/code&gt; and &lt;code&gt;@CacheEvict&lt;/code&gt; are both AOP proxies, and Spring does not guarantee which one runs first unless you set the order explicitly. Stack them on the same method and let Spring pick, and you get eviction and commit racing each other inside a single call. If the eviction fires first and the transaction rolls back afterward, the outcome is harmless: the cache is empty, and the next read repopulates it correctly from the DB. The dangerous direction is the other one. A method that writes to two cache keys and only one of them sits behind the failed step keeps the untouched write in place, because a transaction rollback undoes the database, not the cache. The &lt;code&gt;afterCommit&lt;/code&gt; pattern above closes both directions at once. The evict call cannot fire before the transaction is durable, and it never fires if the transaction dies. That is the actual reason to register a &lt;code&gt;TransactionSynchronization&lt;/code&gt; instead of stacking annotations and trusting Spring to run them in the order you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 2: the pod that wakes up already lying
&lt;/h2&gt;

&lt;p&gt;Kubernetes rolling deployments and autoscaling mean your fleet of instances is never actually static. New pods come up cold. Old ones go away mid-request. And there's a window during pod startup that most cache architecture diagrams pretend doesn't exist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F97cyt7r1rjsod3u8jhu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F97cyt7r1rjsod3u8jhu5.png" alt=" " width="800" height="320"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;diagram 2: Timeline showing a two second gap between a pod passing its readiness probe and its pub/sub subscription becoming active&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Any invalidation broadcast during that window between traffic starting and the subscription connecting is silently lost, and the pod won't know until its TTL backstop expires.&lt;/p&gt;

&lt;p&gt;Here's the timeline. At T0, the container starts. Somewhere around T0 plus 2 seconds, the readiness probe passes and Kubernetes starts routing real traffic to it. But the pod's Redis pub/sub subscription, the one it needs in order to hear "hey, this key was just invalidated," might not finish connecting until T0 plus 4 seconds. That's a two-second window where the pod is serving live reads with an empty local cache and no way to hear about invalidations happening elsewhere in the cluster.&lt;/p&gt;

&lt;p&gt;If a write happens on another pod during that window and broadcasts an invalidation event, this pod never receives it, because it wasn't listening yet. It'll cache whatever it reads from the DB at that moment (which might already be correct) but it has no mechanism to know that a subsequent invalidation was meant for a key it hadn't even cached. The gap compounds if the write pattern is bursty right around a deploy, which, deploys being deploys, it often is.&lt;/p&gt;

&lt;p&gt;What actually mitigates this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't mark the pod ready until the pub/sub client reports itself connected, not just until the app context loads. Wire this into your readiness probe explicitly.&lt;/li&gt;
&lt;li&gt;Treat the local L1 cache (Caffeine, say) as advisory and short-lived. A 30 to 60 second TTL on L1, backed by Redis as L2, means a missed invalidation during startup costs you at most a minute of staleness instead of an indefinite one.&lt;/li&gt;
&lt;li&gt;Log every pub/sub connection event with a timestamp relative to pod start. If you've never measured this window in your own cluster, you don't actually know if it's 200 milliseconds or 4 seconds, and the fix you need depends entirely on which one it is.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Scenario 3: the race Spring has known about since 2012
&lt;/h2&gt;

&lt;p&gt;This one isn't new. It's filed as &lt;a href="https://github.com/spring-projects/spring-framework/issues/13942" rel="noopener noreferrer"&gt;Spring Framework issue SPR-9304&lt;/a&gt;, declined rather than fixed. If you're relying on &lt;code&gt;@Cacheable&lt;/code&gt; and &lt;code&gt;@CacheEvict&lt;/code&gt; to coordinate with each other, they don't, not by default, and the framework maintainers have said as much for over a decade.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1rnlscitemk1d1sqcthd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1rnlscitemk1d1sqcthd.png" alt=" " width="800" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;diagram 3: Timeline of a slow GET reading a stale value and overwriting a cache after a concurrent PUT already evicted it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;DB says APPROVED. Cache says PENDING. Nothing threw an error. This is the exact race described in Spring's SPR-9304, declined rather than fixed.&lt;/p&gt;

&lt;p&gt;Walk through it in order. Thread 1 starts a slow &lt;code&gt;SELECT&lt;/code&gt; at t=0ms, maybe it's hitting an unindexed column or waiting on a lock. While that read is in flight, Thread 2 runs a &lt;code&gt;PUT&lt;/code&gt;: the &lt;code&gt;UPDATE&lt;/code&gt; commits at t=20ms, and the &lt;code&gt;@CacheEvict&lt;/code&gt; fires at t=21ms, clearing the cache entry. Everything looks fine at this point. The cache is empty, which is a safe state.&lt;/p&gt;

&lt;p&gt;Then, at t=120ms, Thread 1's slow read finally returns. It read the row before the update happened, so it's holding the old value. Spring's &lt;code&gt;@Cacheable&lt;/code&gt; does exactly what it's designed to do: it writes that value into the now-empty cache, at t=121ms. There's no eviction after this point to clean it up, because as far as the system knows, nobody wrote anything new. The cache now holds a stale value indefinitely, right next to a database that has the correct one.&lt;/p&gt;

&lt;p&gt;Nobody threw an exception. Nothing logged an error. The only symptom is a support ticket three days later asking why a customer's order still shows "pending" after they got a confirmation email.&lt;/p&gt;

&lt;p&gt;The slow query is not the only trigger. Plenty of Spring setups route &lt;code&gt;@Transactional(readOnly=true)&lt;/code&gt; reads to a database replica through an &lt;code&gt;AbstractRoutingDataSource&lt;/code&gt;, a standard way to keep load off the primary. That routing turns an ordinary GET into a race against replication lag instead of query cost. The &lt;code&gt;PUT&lt;/code&gt; commits on the primary at t=20ms. A read-only &lt;code&gt;GET&lt;/code&gt; lands on a replica that has not replayed that write yet, reads the old row at t=25ms, and caches it. No slow query, no lock contention, just a replica a few milliseconds behind. This version fires far more often than the slow-query version, because replication lag is a constant background condition of the architecture, not an occasional unlucky query plan. &lt;code&gt;readOnly=true&lt;/code&gt; also switches Hibernate to manual flush mode for that session, so a stray write inside a read-only method fails silently instead of throwing.&lt;/p&gt;

&lt;p&gt;The fix that actually works in practice is a short-TTL safety net combined with versioned reads, not a synchronization primitive bolted onto &lt;code&gt;@Cacheable&lt;/code&gt; (Spring's cache abstraction doesn't give you a clean hook for that without writing your own &lt;code&gt;CacheInterceptor&lt;/code&gt;, which is more surface area than most teams want to maintain). A version column or &lt;code&gt;updated_at&lt;/code&gt; timestamp compared at write time catches the case where the read result is older than the latest write, whether that staleness came from a slow query or a lagging replica, and lets you skip writing it into the cache at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;cacheAwareRead&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;lastKnownVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;versionTracker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLatestVersion&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastKnownVersion&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVersion&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lastKnownVersion&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// this read is older than a write we already know happened&lt;/span&gt;
        &lt;span class="c1"&gt;// do not let it land in the cache&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;cacheManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a small check. It closes a gap that's been sitting open in a widely used framework since 2012, which tells you this isn't a corner case, it's a structural property of caching next to a mutable store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 4: what your frontend doesn't know it doesn't know
&lt;/h2&gt;

&lt;p&gt;Say you've handled all three scenarios above. Your backend cache is now consistent with your database within a reasonable window. There's still one more layer where staleness sneaks back in: the contract between your API and whatever's consuming it.&lt;/p&gt;

&lt;p&gt;A user submits a form. Your frontend gets a 200 back. It then re-fetches the same resource to refresh its view, and that &lt;code&gt;GET&lt;/code&gt; happens to hit a replica or a CDN edge that hasn't picked up the change yet. From the user's point of view, they just submitted something and it didn't take. That's not a backend cache bug. It's a read-your-own-writes problem, and it lives at the API contract layer.&lt;/p&gt;

&lt;p&gt;The fix is to hand the client something it can use to demand freshness on its next call. An ETag or a version token, returned on the write response and echoed back on the next read, lets the backend decide: is the cached response older than what this client already knows about? If yes, skip the cache and hit the DB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@PutMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders/{id}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;updateOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;OrderUpdateRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;versionToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVersion&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eTag&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;versionToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders/{id}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nd"&gt;@RequestHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"If-None-Match"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clientVersion&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;OrderCacheEntry&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderCacheService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;cleanClientVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientVersion&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;clientVersion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\""&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleanClientVersion&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parseLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleanClientVersion&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVersion&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// the client already knows about a write newer than what we have cached&lt;/span&gt;
        &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forceRead&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;eTag&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVersion&lt;/span&gt;&lt;span class="o"&gt;())).&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;eTag&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getVersion&lt;/span&gt;&lt;span class="o"&gt;())).&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It closes a gap that no amount of backend cache tuning touches, because the problem isn't in the backend. It's in the assumption that the client and server agree on what "current" means.&lt;/p&gt;

&lt;p&gt;One line in that handler is doing quiet, necessary work. The HTTP spec wraps ETag values in quotes, so &lt;code&gt;If-None-Match&lt;/code&gt; arrives as &lt;code&gt;"12345"&lt;/code&gt;, not &lt;code&gt;12345&lt;/code&gt;. Passing that straight into &lt;code&gt;Long.parseLong&lt;/code&gt; throws &lt;code&gt;NumberFormatException&lt;/code&gt; on every single request, which is the kind of bug that survives code review because a manual curl test with a bare number in the header works fine and the quotes only show up when a real browser or HTTP client sends them. Stripping the quotes before parsing is the difference between this shipping and this getting reverted after the first production 500.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist worth pinning to the PR template
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Does &lt;code&gt;@CacheEvict&lt;/code&gt; call happen after transaction commit, with a retry on failure, or does a network blip leave it silently unevicted?&lt;/li&gt;
&lt;li&gt;Do you have a TTL backstop on every cache entry, even the ones you explicitly evict, sized to two or three times your typical write frequency for that entity?&lt;/li&gt;
&lt;li&gt;Have you measured the gap between the pod's readiness probe passing and its pub/sub client actually connecting? If you haven't measured it, you don't know if it's 200ms or 4 seconds.&lt;/li&gt;
&lt;li&gt;Is L1 (local, in-process) cache TTL short enough that a missed invalidation during a pod's startup window costs you a minute, not an hour?&lt;/li&gt;
&lt;li&gt;Does a slow read that started before a write, but finishes after it, have any way to detect that it's about to write stale data back into the cache?&lt;/li&gt;
&lt;li&gt;Does the API return a version token or ETag on writes, and does the read path actually check it against the client's last known version, or does it just cache-and-serve regardless?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are exotic. The expensive part isn't writing the fix. It's noticing the gap exists before a customer does.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>kubernetes</category>
      <category>redis</category>
    </item>
    <item>
      <title>Oracle Services &amp; Port 1521: A DBA Refresher on Listeners, Services, and Failover</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Thu, 23 Jul 2026 21:19:59 +0000</pubDate>
      <link>https://dev.to/prasadmk/oracle-services-port-1521-a-dba-refresher-on-listeners-services-and-failover-23k8</link>
      <guid>https://dev.to/prasadmk/oracle-services-port-1521-a-dba-refresher-on-listeners-services-and-failover-23k8</guid>
      <description>&lt;p&gt;Every Oracle DBA has typed &lt;code&gt;sqlplus user/pass@host:1521/orclpdb1&lt;/code&gt; a thousand times without stopping to ask what's actually happening on the other end of that colon. It's old material, and it's the kind of old material that quietly underpins every outage post-mortem involving "the app couldn't connect" or "failover didn't work like we expected." Worth a refresher.&lt;/p&gt;

&lt;p&gt;This post covers six things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What actually happens on port 1521&lt;/li&gt;
&lt;li&gt;The design philosophy underneath all of it: decoupling identity from location&lt;/li&gt;
&lt;li&gt;Why one database ends up with &lt;em&gt;multiple&lt;/em&gt; services&lt;/li&gt;
&lt;li&gt;How the listener tells that traffic apart&lt;/li&gt;
&lt;li&gt;What really happens when a service fails over&lt;/li&gt;
&lt;li&gt;Given a service name, how to find the host it's actually running on&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Port 1521 is a door, not a database
&lt;/h2&gt;

&lt;p&gt;Port 1521 is the default TCP port for the &lt;strong&gt;Oracle Net Listener&lt;/strong&gt;, not for the database itself. The listener is a separate process (&lt;code&gt;tnslsnr&lt;/code&gt;) that sits in front of one or more database instances and brokers new connections. It doesn't execute SQL, it doesn't hold data, and it doesn't even have to run on the same machine as the database.&lt;/p&gt;

&lt;p&gt;Here's the flow for a typical connection:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5b2w9sasltsap3tmqhq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi5b2w9sasltsap3tmqhq.png" alt="Listener and port 1521 architecture" width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;figure 1: Listener and port 1521 architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few details worth re-remembering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The client never asks for "the database."&lt;/strong&gt; It asks for a &lt;strong&gt;service name&lt;/strong&gt; (&lt;code&gt;SERVICE_NAME=orclpdb1&lt;/code&gt;), resolved either from &lt;code&gt;tnsnames.ora&lt;/code&gt;, an LDAP/OID directory, or an EZConnect string like &lt;code&gt;host:1521/orclpdb1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services register themselves with the listener.&lt;/strong&gt; The listener doesn't discover them on its own. This happens two ways:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic registration&lt;/strong&gt;: the instance's &lt;code&gt;PMON&lt;/code&gt; background process registers every ~60 seconds (or immediately on startup/shutdown) with any listener it knows about via the &lt;code&gt;local_listener&lt;/code&gt; / &lt;code&gt;remote_listener&lt;/code&gt; parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static registration&lt;/strong&gt;: an explicit &lt;code&gt;SID_LIST_LISTENER&lt;/code&gt; entry in &lt;code&gt;listener.ora&lt;/code&gt;, mostly needed for tools that connect before the instance is fully up (RMAN starting an instance, for example).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Once the listener matches the requested service to a running handler, it hands the socket off (dedicated server mode) or routes it through a dispatcher (shared server mode), and drops out of the picture for the rest of that session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "port 1521" really means the address where the broker for a set of services is listening. It's not a direct pipe into a specific database.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. The design philosophy: decoupling identity from location
&lt;/h2&gt;

&lt;p&gt;Before getting into why multiple services exist, it's worth naming the principle that makes all of it possible, because it's the same principle that explains multiple services, traffic identification, and failover in one shot.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;service&lt;/strong&gt; answers "what workload is this?" An &lt;strong&gt;instance&lt;/strong&gt; answers "what physical process is executing it right now?" Oracle deliberately keeps those two questions independent, so the answer to one can change without touching the other. The relationship between services and instances isn't 1:1. It's many-to-many, and it's meant to be reshaped at runtime.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2tm931m2q5wfclek625.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2tm931m2q5wfclek625.png" alt="Services and instances map many-to-many" width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;figure 2:Services and instances map many-to-many&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That many-to-many mapping delivers four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Location transparency.&lt;/strong&gt; An application connects to &lt;code&gt;sales_svc&lt;/code&gt;, never to "instance 2 on node B." The service name is a stable contract. Which physical instance actually answers it can change hourly, during maintenance, during rebalance, or during failover, and the client's connection string never has to know. It works a lot like a DNS name pointing at a rotating set of IPs, just one layer further up the stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent scaling on both axes.&lt;/strong&gt; Add a new instance (scale out the cluster) and existing services can spread onto it without redefinition. Add a new service (a new workload) and it can be placed on existing instances without adding hardware. If services and instances were rigidly paired, a capacity change on either side would force a redesign of the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy attaches to the logical unit, not the physical one.&lt;/strong&gt; Resource Manager plans, preferred/available instance lists, TAF settings, Data Guard role bindings: all of it is defined &lt;em&gt;on the service&lt;/em&gt;. That's deliberate. Policy should travel with "what this workload is," not with "which machine happens to be running it right now." An instance is disposable infrastructure. A service is the thing that actually has business meaning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instances become interchangeable, fungible capacity.&lt;/strong&gt; Once policy lives on the service side, an instance is just an anonymous unit of CPU and memory that services can be poured into or out of. That's what makes failover, rolling patching, and load rebalancing possible without the application ever noticing. You're relocating the logical unit onto different physical capacity, not rebuilding the workload's identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one-line version: Oracle services turn "which server am I talking to" into a question the database answers for you, by treating instances as fungible capacity and workloads as the durable, addressable, policy-bearing thing. Everything below (dynamic registration, multiple services per database, traffic identification, service relocation) is just the plumbing that keeps that separation true at runtime.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Why does one database need multiple services?
&lt;/h2&gt;

&lt;p&gt;Every Oracle database has always had at least one service (historically identified by the &lt;code&gt;SID&lt;/code&gt;), but real systems almost never stop at one. A few concrete reasons multiple services show up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workload isolation.&lt;/strong&gt; OLTP traffic (short, latency-sensitive transactions) and reporting/batch traffic (long, resource-hungry queries) compete for the same buffer cache and CPU if you let them share a service. Giving each its own service lets you attach different &lt;strong&gt;Resource Manager&lt;/strong&gt; consumer groups, different degrees of parallelism, and different priorities to each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAC placement control.&lt;/strong&gt; In a RAC cluster, a service can be defined with &lt;strong&gt;preferred&lt;/strong&gt; and &lt;strong&gt;available&lt;/strong&gt; instances. That lets you say "this reporting workload only ever runs on node 3, unless node 3 is down" without touching the app's connection string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multitenant (CDB/PDB).&lt;/strong&gt; Every pluggable database gets its own service name automatically. &lt;code&gt;orclpdb1&lt;/code&gt;, &lt;code&gt;orclpdb2&lt;/code&gt;, and so on all live inside one container instance but are addressed, secured, and monitored as fully separate services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-downtime maintenance and versioning.&lt;/strong&gt; You can spin up a new service for a new app version, let both old and new services point at the same schema during a rolling deploy, then retire the old service. No DNS or IP changes required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Guard role routing.&lt;/strong&gt; Active Data Guard setups commonly expose a read-write service (only active on the primary) and a read-only service (active on physical standbys), so applications can be pointed at the correct role without hardcoding "which machine is primary this week."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpz5j1fi16fjg83th0oda.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpz5j1fi16fjg83th0oda.png" alt="Multiple services routing to different workload lanes" width="800" height="462"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;figure 3: Multiple services routing to different workload lanes&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The underlying data files, buffer cache, and SGA are shared. Services are a &lt;strong&gt;software-defined routing and policy layer&lt;/strong&gt; on top of the same physical database, not separate databases.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. How is traffic told apart per service?
&lt;/h2&gt;

&lt;p&gt;This is the part people gloss over: the listener and the database distinguish traffic almost entirely by the &lt;strong&gt;SERVICE_NAME&lt;/strong&gt; string the client presents at connect time. Nothing deeper than that at the network layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The connection string (&lt;code&gt;//host:1521/service_name&lt;/code&gt;) or &lt;code&gt;tnsnames.ora&lt;/code&gt; entry carries the service name in the &lt;code&gt;CONNECT_DATA&lt;/code&gt; section of the TNS descriptor.&lt;/li&gt;
&lt;li&gt;The listener matches that string against its registered service table (visible via &lt;code&gt;lsnrctl services&lt;/code&gt;) and routes the session to an instance/handler currently offering that service.&lt;/li&gt;
&lt;li&gt;Once connected, the session is tagged with that service for its entire lifetime. You can see this live with:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serial&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;session&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SYS$USERS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SYS$BACKGROUND'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;That service tag is what &lt;strong&gt;Resource Manager&lt;/strong&gt; uses to apply consumer-group mappings (&lt;code&gt;DBMS_RESOURCE_MANAGER.SET_CONSUMER_GROUP_MAPPING&lt;/code&gt; with &lt;code&gt;SERVICE_NAME&lt;/code&gt;), what &lt;strong&gt;AWR/ASH&lt;/strong&gt; uses to break down load by workload, and what &lt;strong&gt;Enterprise Manager&lt;/strong&gt; uses to draw separate performance charts per service, all from the same instance.&lt;/li&gt;
&lt;li&gt;Nothing about the SQL, the user, or the schema determines the service. It's purely a connect-time declaration. Two sessions from the same user, same schema, same host, can land in completely different Resource Manager buckets purely because one connected via &lt;code&gt;oltp_svc&lt;/code&gt; and the other via &lt;code&gt;batch_svc&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why service naming discipline matters operationally: a mistyped or reused service name silently puts traffic into the wrong priority bucket, with no error raised anywhere.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. What happens when a service fails over?
&lt;/h2&gt;

&lt;p&gt;"Failover" means different things depending on the layer, but the RAC case is the classic one, so let's walk through it end to end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsph7j4f5cff7gavamll1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsph7j4f5cff7gavamll1.png" alt="Service failover sequence in a RAC cluster" width="800" height="462"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;figure 4: Service failover sequence in a RAC cluster&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detection.&lt;/strong&gt; Clusterware (&lt;code&gt;OCSSD&lt;/code&gt;/&lt;code&gt;CRSD&lt;/code&gt;) detects a node or instance is unreachable via missed heartbeats, not by the listener itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing sessions break.&lt;/strong&gt; Any session connected through the failed instance is gone. Mid-transaction work is lost unless it's covered by &lt;strong&gt;TAF (Transparent Application Failover)&lt;/strong&gt;, &lt;strong&gt;FAN-aware connection pools&lt;/strong&gt;, or a JDBC/UCP replay driver that can transparently re-establish the session and, in some cases, replay in-flight calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification.&lt;/strong&gt; &lt;strong&gt;FAN (Fast Application Notification)&lt;/strong&gt; publishes a service-down event almost immediately (versus clients waiting on a TCP timeout), and it propagates via ONS or the RAC-aware drivers so connection pools can proactively drop dead sessions instead of discovering them on next use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relocation.&lt;/strong&gt; Clusterware relocates the service definition itself via &lt;code&gt;srvctl relocate service&lt;/code&gt; (automatically, per the service's preferred/available instance list), so the service is now offered from a surviving instance. This is a &lt;em&gt;service&lt;/em&gt; moving, not data moving. The underlying database is still the same shared storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnection.&lt;/strong&gt; New connection requests resolve through the &lt;strong&gt;SCAN listener&lt;/strong&gt;, which always points at currently-available instances, so the app doesn't need to know which physical node is now hosting the service. Well-configured connection pools reconnect automatically. Anything with uncommitted work at the time of the failure has to be resubmitted by the application. Oracle can preserve the &lt;em&gt;session&lt;/em&gt;, not the &lt;em&gt;unfinished transaction&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The practical takeaway: failover protects availability of the service, not continuity of in-flight work. If your application logic assumes a failover is invisible, that assumption only holds if you've actually configured TAF/FAN or a replay-capable driver. Plain JDBC thin connections with no retry logic will simply throw errors when their instance disappears.&lt;/p&gt;


&lt;h2&gt;
  
  
  Quick reference commands
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What is the listener actually offering right now?&lt;/span&gt;
lsnrctl services

&lt;span class="c"&gt;# RAC: where is a service running, and what's its preferred/available config?&lt;/span&gt;
srvctl status service &lt;span class="nt"&gt;-d&lt;/span&gt; orclcdb &lt;span class="nt"&gt;-s&lt;/span&gt; sales_svc
srvctl config service &lt;span class="nt"&gt;-d&lt;/span&gt; orclcdb &lt;span class="nt"&gt;-s&lt;/span&gt; sales_svc

&lt;span class="c"&gt;# From SQL: who is connected to which service, right now&lt;/span&gt;
SELECT inst_id, service_name, COUNT&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
FROM gv&lt;span class="nv"&gt;$session&lt;/span&gt; 
GROUP BY inst_id, service_name
ORDER BY inst_id&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Given a service name, how do you find the host?
&lt;/h2&gt;

&lt;p&gt;Since services are decoupled from location by design, there's no single universal reverse lookup, but you can trace it depending on what access you have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you have DB access, query it directly:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inst_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;gv&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;active_services&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;gv&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inst_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inst_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sales_svc'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gv$active_services&lt;/code&gt; (or &lt;code&gt;v$active_services&lt;/code&gt; on a single instance) shows which instance(s) currently offer the service. &lt;code&gt;gv$instance&lt;/code&gt; maps &lt;code&gt;inst_id&lt;/code&gt; to an actual &lt;code&gt;host_name&lt;/code&gt;. This works for both single-instance and RAC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If it's RAC, ask Clusterware directly:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;srvctl status service &lt;span class="nt"&gt;-d&lt;/span&gt; orclcdb &lt;span class="nt"&gt;-s&lt;/span&gt; sales_svc
srvctl config service &lt;span class="nt"&gt;-d&lt;/span&gt; orclcdb &lt;span class="nt"&gt;-s&lt;/span&gt; sales_svc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status&lt;/code&gt; gives the current host. &lt;code&gt;config&lt;/code&gt; gives the preferred/available instance list, so you also know where the service could fail over to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you only have listener access, not DB access:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsnrctl services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows which services and instances that specific listener knows about, but only reflects that one listener's view, not necessarily the full picture in a SCAN setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're starting from just a connection string:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HOST&lt;/code&gt; in &lt;code&gt;tnsnames.ora&lt;/code&gt; or an EZConnect string is what the client resolves first, but in a RAC/SCAN setup this is usually the SCAN listener's hostname, not the actual instance host. You still need the SQL or &lt;code&gt;srvctl&lt;/code&gt; approach above to find where it's really running.&lt;/p&gt;

&lt;p&gt;The practical takeaway: &lt;code&gt;gv$active_services&lt;/code&gt; joined to &lt;code&gt;gv$instance&lt;/code&gt; is the fastest, most accurate reverse lookup when you have DB access. &lt;code&gt;srvctl status service&lt;/code&gt; is the equivalent when you only have OS-level Clusterware access.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick reference commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What is the listener actually offering right now?&lt;/span&gt;
lsnrctl services

&lt;span class="c"&gt;# RAC: where is a service running, and what's its preferred/available config?&lt;/span&gt;
srvctl status service &lt;span class="nt"&gt;-d&lt;/span&gt; orclcdb &lt;span class="nt"&gt;-s&lt;/span&gt; sales_svc
srvctl config service &lt;span class="nt"&gt;-d&lt;/span&gt; orclcdb &lt;span class="nt"&gt;-s&lt;/span&gt; sales_svc

&lt;span class="c"&gt;# From SQL: who is connected to which service, right now&lt;/span&gt;
SELECT inst_id, service_name, COUNT&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
FROM gv&lt;span class="nv"&gt;$session&lt;/span&gt; 
GROUP BY inst_id, service_name
ORDER BY inst_id&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;None of this is new. Services and port 1521 have worked this way since 8i/9i, and RAC service relocation since 10g. But it's exactly the kind of foundational detail that's easy to half-remember, which is where "the failover didn't behave like I expected" incidents come from. Worth keeping the mental model sharp: one port, many services, each one a routable, policy-carrying unit that can move independently of the data it sits on top of, because instances are fungible capacity and services are the durable identity layered on top of them.&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>dba</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>The Counter-Press Architecture: How Distributed Systems Intercept Failures at the Edge</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Tue, 21 Jul 2026 15:53:46 +0000</pubDate>
      <link>https://dev.to/prasadmk/what-spains-counter-press-against-argentina-teaches-us-about-system-design-1acn</link>
      <guid>https://dev.to/prasadmk/what-spains-counter-press-against-argentina-teaches-us-about-system-design-1acn</guid>
      <description>&lt;h3&gt;
  
  
  Introduction
&lt;/h3&gt;

&lt;p&gt;In football (soccer), a Gegenpress (counter-press) isn't merely a defensive tactic, it's an aggressive, high-speed offensive trap. The moment a team loses possession in the attacking third, they don't retreat into a low block to regroup. Instead, nearby players immediately swarm the ball carrier, exploit the opponent's brief moment of disorganization, and strike before the opposing team can structure their defense.&lt;/p&gt;

&lt;p&gt;Spain lost the ball in Argentina's final third during the 2026 World Cup. Within two seconds, three Spanish players had closed down the ball carrier. Nobody dropped back to reorganize. Nobody waited for Argentina to build an attack. They swarmed the loss and had the ball back before Argentina's shape even reformed.&lt;/p&gt;

&lt;p&gt;That's a counter-press, or gegenpressing if you want the German. Calling it a defensive tactic undersells it. The moment a team loses the ball in the attacking third, nearby players swarm the carrier, exploit the few seconds of disorganization, and strike before the opponent can settle into anything resembling a defense.&lt;/p&gt;

&lt;p&gt;I watched that sequence against Argentina and kept thinking about a rule I use constantly in system design: don't fall back, intercept early. The cheapest place to fix a problem is where it starts, not three hops downstream after it's had time to spread.&lt;/p&gt;

&lt;p&gt;In distributed systems and resilient software design, the exact same principle applies: &lt;strong&gt;intercepting an unexpected state transition or failure at the edge before it cascades downstream.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are three patterns from distributed systems that run on the exact same logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit breakers and speculative fallbacks
&lt;/h2&gt;

&lt;p&gt;Picture a low block defense: an API dependency fails or times out, and the system's answer is to let the error bubble all the way up the call stack. That triggers context switches, retry loops, and expensive error handling three layers away from where the problem actually happened. It's the software equivalent of retreating into your own box and hoping the other team doesn't find the gap.&lt;/p&gt;

&lt;p&gt;A counter-pressing architecture skips that. A service mesh or local circuit breaker catches the timeout right at the gateway and serves pre-cached or speculative data on the spot. The request resolves before the rest of the system even knows something failed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Soccer&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ball loss in the attacking third&lt;/td&gt;
&lt;td&gt;API timeout or dependency failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retreat to a low block&lt;/td&gt;
&lt;td&gt;Error bubbles up the call stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swarm the ball carrier&lt;/td&gt;
&lt;td&gt;Circuit breaker trips at the edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Win it back and counter&lt;/td&gt;
&lt;td&gt;Serve cached data before the client notices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Recovering isn't the hard part. Recovering before anyone downstream notices is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimistic concurrency with fast-path retries
&lt;/h2&gt;

&lt;p&gt;Pessimistic locking is a low block for databases. Threads want the same resource, so you lock it early, put everyone else to sleep, and make them wait their turn. It works. It's also slow, and it gives up ground you didn't need to give up.&lt;/p&gt;

&lt;p&gt;Optimistic concurrency control plays a higher line. Threads assume no conflict, because most of the time there isn't one. When a commit does hit a collision, the system doesn't sleep the thread or force a context switch. It fires an immediate spin-retry right at the L1/L2 cache boundary, while the data is still warm in memory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Soccer&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ball loose in midfield&lt;/td&gt;
&lt;td&gt;Lock or version conflict detected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sit deep and defend&lt;/td&gt;
&lt;td&gt;Thread sleeps, context switch triggered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Press instantly while the position favors you&lt;/td&gt;
&lt;td&gt;In-cache spin-lock retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Win it back before the opponent regroups&lt;/td&gt;
&lt;td&gt;Commit completes before other threads catch up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That window closes fast. Wait even a beat too long and the cache goes cold, same as a press that arrives a half-second late against a defense that's already reset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backpressure as interception, not a wall
&lt;/h2&gt;

&lt;p&gt;A fast producer overwhelming a slow consumer is a familiar problem in stream processing. Do nothing about it, and the buffer fills up, packets start dropping, and eventually something crashes. That's a defense getting run over because nobody pressed the ball early enough to matter.&lt;/p&gt;

&lt;p&gt;A counter-pressing stream architecture applies backpressure at the producer itself, before the surge ever reaches the broker. Reactive operators throttle the source, route the overflow to a dead-letter queue, and keep the rest of the pipeline running without a full rebalance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Soccer&lt;/th&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opponent building an attack&lt;/td&gt;
&lt;td&gt;Producer throughput spikes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wait for it to reach your box&lt;/td&gt;
&lt;td&gt;Buffer fills, packets drop downstream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Press the source immediately&lt;/td&gt;
&lt;td&gt;Backpressure applied at the edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force a turnover before it develops&lt;/td&gt;
&lt;td&gt;Overflow routed to a dead-letter queue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why this keeps showing up
&lt;/h2&gt;

&lt;p&gt;None of these three patterns are related to each other technically. Circuit breakers, OCC, and backpressure solve different problems in different layers of a system. What they share is where they choose to act.&lt;/p&gt;

&lt;p&gt;A retry loop three services downstream costs you latency and a confused client. A circuit breaker at the edge costs you milliseconds. A thread that sleeps and context-switches costs a full scheduler round trip. A spin-lock retry costs a handful of cache cycles. A buffer that overflows three hops down costs you dropped data and a painful rebalance. Backpressure at the source costs you one throttle signal.&lt;/p&gt;

&lt;p&gt;Same failure, wildly different price depending on where you catch it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F952llbco9zhw1chb98vc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F952llbco9zhw1chb98vc.png" alt=" " width="800" height="1071"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: The Gegenpress tactic mapped to resilient system design patterns.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Spain's press works for the same reason. Winning the ball back isn't really the goal. Winning it back while the opponent is still disorganized, before they've had a second to reset, is what turns a turnover into a goal. Catch failure, contention, or overload as close to the source as you can, while the state is still cheap to recover, and you get the software version of the same thing: a problem that never gets the chance to become a bigger problem.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Locking Down the Pipeline: Enforcing Contract Integrity Against Autonomous AI Agents</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Sat, 30 May 2026 18:01:22 +0000</pubDate>
      <link>https://dev.to/prasadmk/locking-down-the-pipeline-enforcing-contract-integrity-against-autonomous-ai-agents-m5m</link>
      <guid>https://dev.to/prasadmk/locking-down-the-pipeline-enforcing-contract-integrity-against-autonomous-ai-agents-m5m</guid>
      <description>&lt;p&gt;Parts 1 through 3 assumed one thing: a human is in the loop. A developer runs the local gate, reads the failure, and makes a deliberate decision. Even in Part 3, the vibe coder is still present. They feed the spec to the AI, read the output, and decide whether to push.&lt;/p&gt;

&lt;p&gt;Part 4 removes that assumption entirely.&lt;/p&gt;

&lt;p&gt;Autonomous AI agents, tools like Devin, AutoGPT, or custom LangChain pipelines, can now write code, run tests, interpret failures, and open pull requests without a human reviewing each step. This is not a future scenario. Teams are already running these workflows today.&lt;/p&gt;

&lt;p&gt;The drift problem does not disappear in this environment. It accelerates. And it gets a new capability: the ability to cover its own tracks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Autonomous Agents Break the Framework
&lt;/h2&gt;

&lt;p&gt;An AI agent tasked with a refactor will do whatever it takes to satisfy the local objective. If it changes the pagination logic and the REST Assured test fails, it does not stop and ask a developer for guidance. It looks at the failure, determines that the test is an obstacle, and rewrites the assertion to make the build green.&lt;/p&gt;

&lt;p&gt;From the agent's perspective, the task is complete. The build passes. The PR opens.&lt;/p&gt;

&lt;p&gt;From the system's perspective, the contract was just silently redefined by an automated process that had no awareness of downstream consumers, no knowledge of the versioning rules from Part 2, and no constraint preventing it from touching protected files.&lt;/p&gt;

&lt;p&gt;The governance framework built in Parts 1 and 2 relied on human judgment at the decision point. CODEOWNERS works when a human reviewer looks at the PR. A verbal rule about not mutating tests works when a developer reads it and understands why it exists.&lt;/p&gt;

&lt;p&gt;Neither of these holds when the contributor is an agent running at machine speed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Programmatic Rails, Not Prose Rules
&lt;/h2&gt;

&lt;p&gt;You cannot solve an automated problem with a social solution. Telling an AI agent to follow the rules in its system prompt is not a governance strategy. Context windows drift. Model updates change behavior. Prompt instructions get deprioritized when the agent is focused on satisfying a local objective.&lt;/p&gt;

&lt;p&gt;The framework needs to be deterministic. The rails need to be structural. The enforcement needs to happen at the system level, not the prompt level.&lt;/p&gt;

&lt;p&gt;Three layers make this work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: The Constraint File as a Machine-Readable Contract
&lt;/h2&gt;

&lt;p&gt;The first layer moves the governance rules out of prose and into a structured file that the agent is required to parse before acting.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;.ai-rules.json&lt;/code&gt; file at the root of the repository:&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;"repository_constraints"&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;"api_versioning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STRICT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"breaking_changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NEVER_MUTATE_EXISTING_TESTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"known_domain_rules"&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;"/api/v1/users"&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;"pagination_base"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"enforced_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UserWorkflowVerificationTest.java"&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="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;This file does two things. It tells the agent exactly which domain rules govern each endpoint, and it explicitly names the test file that enforces each rule. When the agent is tasked with modifying anything under &lt;code&gt;/api/v1/users&lt;/code&gt;, it must parse &lt;code&gt;known_domain_rules&lt;/code&gt; first and treat those constraints as non-negotiable inputs, not suggestions.&lt;/p&gt;

&lt;p&gt;The critical shift here is the difference between a rule the agent reads and a rule the agent loads as structured data. Prose in a system prompt gets weighed against the agent's objective. A JSON constraint file that the orchestration script injects into the agent's context window before execution is a boundary condition, not a preference.&lt;/p&gt;

&lt;p&gt;Commit this file to the repository and version it alongside the API. When the contract evolves, the constraint file evolves with it in the same PR. The rules are traceable, reviewable, and visible to every contributor, human or automated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2: The Pre-Commit Hook as the Deterministic Bouncer
&lt;/h2&gt;

&lt;p&gt;The constraint file sets expectations. The pre-commit hook enforces them at the moment the agent tries to commit.&lt;/p&gt;

&lt;p&gt;This is the most important layer because it operates entirely outside the agent's control. No matter what the agent decided to do, no matter what it changed, the hook runs before the commit is allowed to proceed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Contract integrity check - runs before every commit&lt;/span&gt;

&lt;span class="c"&gt;# Step 1: Run the protected REST Assured contract tests&lt;/span&gt;
mvn &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-Dtest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;UserWorkflowVerificationTest

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: Contract tests failed. The commit is blocked."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Fix the underlying code. Do not modify the test assertions."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: Verify the agent did not touch the protected test file&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"UserWorkflowVerificationTest.java"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: AI Agent attempted to modify a protected contract test."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Functional changes require a new API version or architectural sign-off."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook does two things in sequence. It runs the contract tests and blocks the commit if they fail. Then it checks the git diff and blocks the commit if the agent touched the protected test file at all, regardless of whether the tests pass.&lt;/p&gt;

&lt;p&gt;That second check is the one that matters most. An agent that rewrites the assertion to make a failing test pass will produce a green test result. Without the diff check, the first gate would not catch it. The combination of both checks closes that gap entirely.&lt;/p&gt;

&lt;p&gt;If the agent cannot commit, it cannot open a PR. If it cannot open a PR, the drift never reaches the repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3: The Coder and Auditor Pattern
&lt;/h2&gt;

&lt;p&gt;The two layers above are defensive. They block bad commits from landing. The third layer is diagnostic. It determines why a failure happened and instructs the agent on how to fix it correctly.&lt;/p&gt;

&lt;p&gt;The pattern is a separation of roles. Agent A is the Coder. It writes and refactors code. Agent B is the Auditor. It reviews the delta when the gate fails. These are two distinct LLM instances with different objectives, and they must never be the same instance self-reviewing its own output.&lt;/p&gt;

&lt;p&gt;The reason this separation matters is the same reason a developer should not approve their own PR. An agent asked to both write code and verify its correctness will optimize for satisfying its own objective. The Auditor needs to be a genuinely independent process with a different prompt, a different focus, and explicit authority to reject the Coder's output.&lt;/p&gt;

&lt;p&gt;When the pre-commit hook fails, the orchestration layer triggers the Auditor with this prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System: You are an automated architectural gatekeeper.
Your job is to determine if a code change introduced a breaking
regression or a valid feature expansion.

Input Artifacts:
1. Git diff of the change: [Insert diff]
2. Test failure log: [Insert REST Assured terminal output]
3. Enforced rules schema: [Insert .ai-rules.json contents]

Task: Analyze whether the code change violates any constraint
defined in the rules schema. If it does, generate a rejection
log that instructs the Coder agent to revert the specific change
and fix the underlying logic.

Constraint: Under no circumstances should the test suite assertions
be modified. The tests define the contract. The code must conform to them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Auditor does not fix the code. It produces a rejection log that describes exactly what the Coder did wrong and what it needs to do differently. The Coder then receives that rejection log as its next input and retries.&lt;/p&gt;

&lt;p&gt;This loop continues until the pre-commit hook passes cleanly, meaning the contract tests are green and the protected test files are untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Full Framework Holds Together
&lt;/h2&gt;

&lt;p&gt;Stepping back across all four parts, the same contract flows through every layer.&lt;/p&gt;

&lt;p&gt;The Spring Boot application exposes its live OpenAPI spec at &lt;code&gt;/v3/api-docs&lt;/code&gt;. REST Assured derives its tests from that contract. Postman derives its collections from that contract. CODEOWNERS enforces that nobody modifies the core test files without cross-team review. API versioning ensures that behavioral changes ship as new endpoints, not as silent mutations to existing ones. The &lt;code&gt;.ai-rules.json&lt;/code&gt; file encodes the domain rules as machine-readable constraints. The pre-commit hook enforces those constraints at commit time, regardless of whether the contributor is human or automated. The Auditor agent closes the diagnostic loop when something goes wrong.&lt;/p&gt;

&lt;p&gt;At no point does the framework rely on trust, memory, or discipline. Every layer is structural. Every enforcement is deterministic. The contract is defined once, in the code, and every tool downstream, whether it is a developer, a vibe coder, or an autonomous agent, operates within the same boundaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing the Series
&lt;/h2&gt;

&lt;p&gt;The zero-drift problem is not a tooling problem. The tools, REST Assured, Postman, Git, OpenAPI, were always capable of solving it. The missing piece was a coherent framework that connected them into a single chain of enforcement, from the individual developer's local machine all the way to an autonomous agent operating without human oversight.&lt;/p&gt;

&lt;p&gt;That chain is now complete. Start with Part 1 today. The local loop costs less than an hour to set up and pays back immediately. Add the governance layer from Part 2 when the team grows. Introduce the AI prompt discipline from Part 3 when AI tools enter the workflow. Apply the programmatic rails from Part 4 when agents start opening PRs on their own.&lt;/p&gt;

&lt;p&gt;The build should be green because the contract is intact. Every time. At every scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Zero-Drift API Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-zero-drift-api-series-stop-trusting-a-green-build-you-cant-explain-k56"&gt;Intro: The Zero-Drift API Series: Stop Trusting a Green Build You Can't Explain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Part 1: A Guide to Stop Breaking Merges: Unifying Postman and REST Assured in Spring Boot&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/who-approved-this-change-managing-api-contracts-and-test-rot-in-large-engineering-teams-39"&gt;Part 2: Who Approved This Change? Managing API Contracts and Test Rot in Large Engineering Teams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-ai-superpower-how-vibe-coders-use-openapi-as-a-semantic-anchor-45kg"&gt;Part 3: The AI Superpower: How Vibe Coders Use OpenAPI as a Semantic Anchor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/locking-down-the-pipeline-enforcing-contract-integrity-against-autonomous-ai-agents-m5m"&gt;Part 4: Locking Down the Pipeline: Enforcing Contract Integrity Against Autonomous AI Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>The AI Superpower: How Vibe Coders Use OpenAPI as a Semantic Anchor</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Sat, 30 May 2026 17:53:16 +0000</pubDate>
      <link>https://dev.to/prasadmk/the-ai-superpower-how-vibe-coders-use-openapi-as-a-semantic-anchor-45kg</link>
      <guid>https://dev.to/prasadmk/the-ai-superpower-how-vibe-coders-use-openapi-as-a-semantic-anchor-45kg</guid>
      <description>&lt;p&gt;Parts 1 and 2 solved the drift problem for developers who write code deliberately. A local REST Assured gate, Postman wired to the live spec, CODEOWNERS enforcing review on core contracts, and API versioning as the hard rule when behavior changes.&lt;/p&gt;

&lt;p&gt;Now introduce a different kind of contributor. A vibe coder.&lt;/p&gt;

&lt;p&gt;A vibe coder is not a junior developer making rookie mistakes. They are often highly productive engineers who use AI tools like Cursor, Copilot, or a plain LLM chat window to generate, refactor, and iterate on code rapidly. The focus is on intent and outcome, not on typing syntax. The speed is real. So is the risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem AI Introduces
&lt;/h2&gt;

&lt;p&gt;An AI model generating code has no awareness of your running system. It does not know that your pagination is 1-based, that a specific field was renamed three sprints ago, or that a downstream mobile client breaks if a response envelope changes shape.&lt;/p&gt;

&lt;p&gt;It knows patterns. It knows what a Spring Boot controller generally looks like. It knows what a TypeScript fetch service should probably contain. When it does not have exact information, it fills the gap with a confident-looking guess.&lt;/p&gt;

&lt;p&gt;This is called a hallucinated contract. The generated code compiles. The types look plausible. The field names are reasonable. And the first time it runs against your actual API, something quietly breaks because the AI invented a payload shape that does not match reality.&lt;/p&gt;

&lt;p&gt;The drift problem from Part 1 gets faster and more confident with AI in the loop. Without a semantic anchor, the AI is just a very fluent source of test rot.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Feed the Spec, Not Just the Prompt
&lt;/h2&gt;

&lt;p&gt;The OpenAPI spec that has been doing all the work in Parts 1 and 2 is the answer here too. Instead of asking the AI to guess your contract, you hand it the exact live definition from your running application and treat it as a non-negotiable constraint.&lt;/p&gt;

&lt;p&gt;When the AI works from &lt;code&gt;/v3/api-docs&lt;/code&gt;, it is no longer guessing. It knows the exact endpoint paths, the required fields, the optional fields, the data types, the pagination parameters, and the response envelope shape. Every piece of code it generates is grounded in what the system actually does right now.&lt;/p&gt;

&lt;p&gt;This is the semantic anchor. The same single source of truth that keeps Postman and REST Assured in sync becomes the context that keeps AI-generated code from drifting before it is even written.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Prompts That Close the Loop
&lt;/h2&gt;

&lt;p&gt;The following prompts cover the three stages where AI intersects with the zero-drift workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt 1: Generate the REST Assured Test Suite from the Live Spec
&lt;/h3&gt;

&lt;p&gt;Instead of writing boilerplate test classes by hand, boot the application, grab the OpenAPI JSON from &lt;code&gt;http://localhost:8080/v3/api-docs&lt;/code&gt;, and hand it to the AI with this prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System: You are an expert backend QA engineer.
I will provide my local application's raw OpenAPI/Swagger JSON specification.

Task: Generate a complete REST Assured integration test class in Java
using @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT).

Rules:
1. Use standard given().when().then() syntax.
2. Every endpoint in the specification must have at least one contract
   validation test covering the status code and core payload fields.
3. Implement random local port setup using @LocalServerPort.

Here is the OpenAPI specification JSON:
[Paste http://localhost:8080/v3/api-docs output here]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI now generates tests that match your actual endpoints, your actual field names, and your actual response structure. Not a plausible approximation of them.&lt;/p&gt;

&lt;p&gt;This also means the tests are immediately runnable. There is no cleanup pass to fix field names the AI invented.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt 2: Debug a Failing Test Without Reading the Stack Trace
&lt;/h3&gt;

&lt;p&gt;When a local REST Assured test fails after an AI-assisted refactor, the natural instinct is to dig through the stack trace manually. There is a faster path. Feed the failure back to the AI with the relevant controller code and let it diagnose itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Context: My local REST Assured test suite just failed during a local check.

The Error:
[Paste the JUnit failure output here, e.g.:
 JSON path currentPage expected 1 but was 2]

The Relevant Controller/Service Code:
[Paste your Spring Boot Controller or Service class here]

Task: Analyze the code and the test failure. Identify where the contract
logic diverged. Fix the underlying Java code to restore backward
compatibility with the expected contract. Explain exactly why the
runtime behavior shifted.

Constraint: Do not modify the test assertion. The test reflects the
contract. The code must conform to it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraint at the end is critical and worth stating explicitly every time. Without it, the AI's default instinct is to make the test pass by any means available, including rewriting the assertion. That is exactly the silent mutation problem from Part 2. The prompt structure prevents it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompt 3: Generate Frontend Code with Zero Payload Drift
&lt;/h3&gt;

&lt;p&gt;When a vibe coder moves to building a UI component or a mobile client that consumes the Spring Boot API, the same anchor applies. Do not let the AI guess the payload shape. Give it the spec.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Context: I am building a frontend component in React/TypeScript that
communicates with my Spring Boot backend.

Input: Here is the live contract definition from my backend:
[Paste http://localhost:8080/v3/api-docs output here]

Task: Generate a TypeScript Fetch/Axios service and the corresponding
interface types for the /api/v1/users endpoint.

Rules:
1. All property names must match the schema definitions exactly.
2. Respect optional and required field flags from the spec.
3. Pagination handling must be based strictly on the parameters
   defined in the specification. Do not assume defaults.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a TypeScript service that matches the backend contract on the first generation. No runtime surprises when the frontend hits the actual API. No field name mismatches discovered in a browser console after a deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters Beyond Convenience
&lt;/h2&gt;

&lt;p&gt;There is a deeper point here worth stating plainly.&lt;/p&gt;

&lt;p&gt;The zero-drift framework in Parts 1 and 2 is built on one principle: the contract lives in the code, and everything else derives from it. REST Assured derives from it. Postman derives from it. CODEOWNERS enforces that nobody silently redefines it.&lt;/p&gt;

&lt;p&gt;When AI enters the workflow without this anchor, it introduces a third source of truth, which is the model's best guess. That guess is invisible, confident, and wrong in ways that are hard to detect until something breaks downstream.&lt;/p&gt;

&lt;p&gt;Feeding the spec into the AI does not slow down the vibe coder's workflow. It makes the output trustworthy on the first pass, which is actually faster than the debug cycle that follows a hallucinated contract.&lt;/p&gt;

&lt;p&gt;The local REST Assured gate from Part 1 remains the final verification. If the AI introduced drift anywhere, the gate catches it in under five seconds. The developer does not need to know exactly what the AI got wrong. They feed the failure back with Prompt 2, fix the code, and run the gate again.&lt;/p&gt;

&lt;p&gt;The loop is fast. The contract stays clean. The AI becomes an accelerator rather than a liability.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;Parts 1 through 3 handle the developer-in-the-loop case, whether they are writing code manually, working in a large team with governance constraints, or using AI tools to generate at speed.&lt;/p&gt;

&lt;p&gt;Part 4 removes the developer from the loop entirely. When autonomous AI agents are opening pull requests without a human reviewing each change, the governance framework needs to be deterministic and programmatic. Pre-commit hooks, constraint files baked into the repository, and a multi-agent Coder/Auditor pattern that treats the AI like an untrusted contributor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/prasadmk/locking-down-the-pipeline-enforcing-contract-integrity-against-autonomous-ai-agents-m5m"&gt;Part 4: Locking Down the Pipeline: Enforcing Contract Integrity Against Autonomous AI Agents&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Zero-Drift API Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-zero-drift-api-series-stop-trusting-a-green-build-you-cant-explain-k56"&gt;Intro: The Zero-Drift API Series: Stop Trusting a Green Build You Can't Explain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Part 1: A Guide to Stop Breaking Merges: Unifying Postman and REST Assured in Spring Boot&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/who-approved-this-change-managing-api-contracts-and-test-rot-in-large-engineering-teams-39"&gt;Part 2: Who Approved This Change? Managing API Contracts and Test Rot in Large Engineering Teams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-ai-superpower-how-vibe-coders-use-openapi-as-a-semantic-anchor-45kg"&gt;Part 3: The AI Superpower: How Vibe Coders Use OpenAPI as a Semantic Anchor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/locking-down-the-pipeline-enforcing-contract-integrity-against-autonomous-ai-agents-m5m"&gt;Part 4: Locking Down the Pipeline: Enforcing Contract Integrity Against Autonomous AI Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>llm</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>Who Approved This Change? Managing API Contracts and Test Rot in Large Engineering Teams</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Sat, 30 May 2026 17:39:22 +0000</pubDate>
      <link>https://dev.to/prasadmk/who-approved-this-change-managing-api-contracts-and-test-rot-in-large-engineering-teams-39</link>
      <guid>https://dev.to/prasadmk/who-approved-this-change-managing-api-contracts-and-test-rot-in-large-engineering-teams-39</guid>
      <description>&lt;p&gt;Part 1 established a clean local loop. REST Assured runs in under five seconds on the developer's machine. Postman pulls from the live OpenAPI spec. The gate is reliable, and the contract is grounded in actual code.&lt;/p&gt;

&lt;p&gt;Now add 50 developers to that picture.&lt;/p&gt;

&lt;p&gt;Suddenly the local loop is not enough. One developer's intentional optimization is another downstream team's production regression. And here is the uncomfortable truth: the test suite itself becomes the attack surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Trap Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;When a REST Assured test fails because a developer changed API behavior, there are two ways to make it green again. Fix the code, or fix the test.&lt;/p&gt;

&lt;p&gt;Fixing the test is faster. It feels harmless. The build goes green. The PR merges. And somewhere downstream, a team that depended on the original behavior starts seeing failures they did not cause and cannot explain.&lt;/p&gt;

&lt;p&gt;This is not a discipline problem. It is a structural one. When a single developer can silently redefine an API contract by updating an assertion, the test suite stops being a gate and starts being a liability.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Opposite Trap Is Just as Bad
&lt;/h2&gt;

&lt;p&gt;The instinctive overcorrection is to declare all existing tests immutable. Nobody touches them. Ever. Only new tests get added.&lt;/p&gt;

&lt;p&gt;This sounds safe. It is not.&lt;/p&gt;

&lt;p&gt;When an API genuinely needs to evolve, the old test stays active and stays failing. Teams start using &lt;code&gt;@Ignore&lt;/code&gt; just to get deployments through. The test suite becomes a graveyard of outdated assertions that nobody trusts and nobody deletes. This is test rot, and it is just as damaging as silent contract mutation, just slower.&lt;/p&gt;

&lt;p&gt;The dilemma looks like this:&lt;/p&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;The Benefit&lt;/th&gt;
&lt;th&gt;The Trap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Never change old tests&lt;/td&gt;
&lt;td&gt;Prevents silent contract rewrites&lt;/td&gt;
&lt;td&gt;Causes permanent build failures when changes are intentional. The suite becomes a historical graveyard.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freely modify existing tests&lt;/td&gt;
&lt;td&gt;Keeps the suite clean and passing&lt;/td&gt;
&lt;td&gt;One developer can redefine the contract for the entire organization without downstream teams knowing.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither extreme works at scale. The answer is a governance layer that distinguishes between the two cases explicitly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architectural Solution: Three Levers
&lt;/h2&gt;

&lt;p&gt;You do not need to throw away REST Assured or install a complex contract broker to solve this. Three levers, used together, handle the governance problem with tooling teams already have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lever 1: API Versioning as a Hard Rule
&lt;/h3&gt;

&lt;p&gt;When a functional change modifies payload structure, field behavior, or pagination logic, the existing endpoint is not touched. It stays exactly as it is, along with its REST Assured tests.&lt;/p&gt;

&lt;p&gt;The change ships under a new version.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/api/v1/users&lt;/code&gt; and its test suite remain intact and passing. &lt;code&gt;/api/v2/users&lt;/code&gt; is introduced with a brand new test class that reflects the new behavior. Downstream teams consuming v1 are never broken. They migrate to v2 on their own timeline.&lt;/p&gt;

&lt;p&gt;This rule eliminates the core dilemma entirely. There is no decision to make about whether to update an old test, because the old endpoint and its tests are never touched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lever 2: CODEOWNERS to Enforce Review on Core Contracts
&lt;/h3&gt;

&lt;p&gt;GitHub and GitLab both support a &lt;code&gt;CODEOWNERS&lt;/code&gt; file that assigns mandatory reviewers to specific directories. This is the enforcement mechanism for the versioning rule.&lt;/p&gt;

&lt;p&gt;Place the core REST Assured contract tests under a protected path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/test/java/com/yourorg/contracts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a CODEOWNERS entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight codeowners"&gt;&lt;code&gt;&lt;span class="n"&gt;src/test/java/com/yourorg/contracts/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@api-architecture-team&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any PR that modifies a file inside that directory cannot merge without explicit approval from the API architecture group. A developer can freely add new test files for new features anywhere else. But touching an existing contract test requires a deliberate, cross-team sign-off.&lt;/p&gt;

&lt;p&gt;The gate is no longer social. It is structural.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lever 3: Deprecation Windows Instead of Deletion
&lt;/h3&gt;

&lt;p&gt;When old functionality genuinely needs to retire, the process is not a silent test deletion. It is a staged, visible wind-down.&lt;/p&gt;

&lt;p&gt;Mark the old endpoint as &lt;code&gt;@Deprecated&lt;/code&gt; in the Spring Boot controller. Log a deprecation warning on every call so downstream teams see it in their monitoring. Schedule the removal for a future sprint with a communicated deadline. When that sprint arrives, the old endpoint and its REST Assured tests are deleted together, in the same PR, with full visibility.&lt;/p&gt;

&lt;p&gt;Nobody is surprised. Nobody discovers a broken contract in production. The retirement is as explicit as the original contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  When You Need More: Consumer-Driven Contract Testing
&lt;/h2&gt;

&lt;p&gt;The three levers above handle the majority of real-world cases without additional infrastructure. But if your organization has many independent teams consuming the same APIs, and the coordination cost of manual review is becoming a bottleneck, the next step is Consumer-Driven Contract Testing using Pact or Spring Cloud Contract.&lt;/p&gt;

&lt;p&gt;The model works like this. Each downstream team that consumes an API publishes a consumer contract that explicitly declares what it expects. When Team A's developer makes a change that affects &lt;code&gt;/api/v1/users&lt;/code&gt;, the CI pipeline automatically pulls all active consumer contracts from a shared broker and runs them against the new code. If any consumer contract fails, the merge is blocked and the affected team is notified before anything ships.&lt;/p&gt;

&lt;p&gt;The key difference from the lever approach is that enforcement becomes fully automated. No human reviewer needs to catch the conflict. The pipeline catches it.&lt;/p&gt;

&lt;p&gt;This is the right investment when teams are large enough that CODEOWNERS review becomes a bottleneck, or when downstream consumers are external and cannot be coordinated manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;A developer on Team A refactors the user pagination logic and opens a PR.&lt;/p&gt;

&lt;p&gt;Without governance: the test fails, they update the assertion, CI goes green, Team B finds out when their service breaks in staging.&lt;/p&gt;

&lt;p&gt;With governance: the test fails. The CODEOWNERS rule blocks the PR from merging without architecture review. The reviewer looks at the change, determines it is a functional behavioral shift, and asks the developer to version the endpoint. &lt;code&gt;/api/v2/users&lt;/code&gt; is introduced. Team B is notified of the new version. Team A ships. Nobody breaks.&lt;/p&gt;

&lt;p&gt;The difference is not more process. It is the right structure in the right place.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;At this point the framework handles individual developers and distributed teams. The contract is protected, the versioning rule is enforced, and the governance layer is structural rather than cultural.&lt;/p&gt;

&lt;p&gt;Part 3 introduces a different kind of contributor: AI-assisted developers using tools like Cursor or Copilot, who are generating code faster than any review process was designed to handle. The same OpenAPI spec that anchors Postman and REST Assured becomes the semantic anchor that keeps AI-generated code from hallucinating payload shapes and silently drifting from the contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/prasadmk/the-ai-superpower-how-vibe-coders-use-openapi-as-a-semantic-anchor-45kg"&gt;Part 3: The AI Superpower: How Vibe Coders Use OpenAPI as a Semantic Anchor&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Zero-Drift API Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-zero-drift-api-series-stop-trusting-a-green-build-you-cant-explain-k56"&gt;Intro: The Zero-Drift API Series: Stop Trusting a Green Build You Can't Explain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/a-guide-to-stop-breaking-merges-unifying-postman-and-rest-assured-in-spring-boot-42fg"&gt;Part 1: A Guide to Stop Breaking Merges: Unifying Postman and REST Assured in Spring Boot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/who-approved-this-change-managing-api-contracts-and-test-rot-in-large-engineering-teams-39"&gt;Part 2: Who Approved This Change? Managing API Contracts and Test Rot in Large Engineering Teams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-ai-superpower-how-vibe-coders-use-openapi-as-a-semantic-anchor-45kg"&gt;Part 3: The AI Superpower: How Vibe Coders Use OpenAPI as a Semantic Anchor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/locking-down-the-pipeline-enforcing-contract-integrity-against-autonomous-ai-agents-m5m"&gt;Part 4: Locking Down the Pipeline: Enforcing Contract Integrity Against Autonomous AI Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>devops</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
    <item>
      <title>A Guide to Stop Breaking Merges: Unifying Postman and REST Assured in Spring Boot</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Sat, 30 May 2026 17:32:41 +0000</pubDate>
      <link>https://dev.to/prasadmk/a-guide-to-stop-breaking-merges-unifying-postman-and-rest-assured-in-spring-boot-42fg</link>
      <guid>https://dev.to/prasadmk/a-guide-to-stop-breaking-merges-unifying-postman-and-rest-assured-in-spring-boot-42fg</guid>
      <description>&lt;p&gt;Every engineering team hits this wall eventually. A developer updates an endpoint, verifies it quickly on their local machine, and opens a pull request. It merges cleanly. Then the deployment pipeline breaks, or worse, a silent contract regression slips past CI entirely and lands in staging or production.&lt;/p&gt;

&lt;p&gt;This is the Merge Bottleneck. The root cause is almost never a lack of discipline. It is a structural flaw in how validation workflows are separated.&lt;/p&gt;

&lt;p&gt;Manual exploratory testing in Postman does not scale to guard git merges. A detached REST Assured suite that nobody keeps in sync with the actual API introduces test drift. Two tools, two descriptions of the same system, diverging silently over time.&lt;/p&gt;

&lt;p&gt;This guide fixes that by making the Spring Boot application itself the single source of truth, and wiring both Postman and REST Assured to consume from it directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Strategy: Code-First Sync
&lt;/h2&gt;

&lt;p&gt;Spring Boot, with the &lt;code&gt;springdoc-openapi&lt;/code&gt; dependency, exposes a live OpenAPI specification at &lt;code&gt;/v3/api-docs&lt;/code&gt; whenever the application is running. That endpoint is your contract. Everything else derives from it.&lt;/p&gt;

&lt;p&gt;Both Postman and REST Assured point at the same running application. When the code changes, both tools see the change immediately. There is no manual sync step, no documentation page to update, and no drift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Establish the Local Automated Gate
&lt;/h2&gt;

&lt;p&gt;The primary safety net needs to run on the developer's machine in seconds, before any CI queue is involved. Waiting for a pipeline to tell you the build is broken is already too late in the feedback loop.&lt;/p&gt;

&lt;p&gt;The setup uses &lt;code&gt;@SpringBootTest&lt;/code&gt; with &lt;code&gt;RANDOM_PORT&lt;/code&gt;. This spins up a real, isolated instance of the application, runs REST Assured verifications against it, and tears down the context when the test completes. Nothing is mocked. The full application stack runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webEnvironment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpringBootTest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;WebEnvironment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RANDOM_PORT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserWorkflowVerificationTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@LocalServerPort&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@BeforeEach&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RestAssured&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;baseURI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://localhost"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;RestAssured&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;verifyUserCreationContract&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RestAssured&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;given&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{\"name\": \"Dev Team\", \"email\": \"dev@company.com\"}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test is the gate. If it is red, the developer does not push.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Wire Postman to the Live Local Spec
&lt;/h2&gt;

&lt;p&gt;When a developer needs to explore edge cases or visually inspect a payload, they should not be manually constructing JSON from memory or from a Confluence page last updated six months ago.&lt;/p&gt;

&lt;p&gt;The workflow is three steps. Boot the Spring Boot application locally at &lt;code&gt;http://localhost:8080&lt;/code&gt;. Open Postman, select Import, and provide &lt;code&gt;http://localhost:8080/v3/api-docs&lt;/code&gt;. Postman parses the live OpenAPI spec and generates a structured collection that exactly matches the current code state.&lt;/p&gt;

&lt;p&gt;When a data model changes in the Java source, Postman's Update Collection re-syncs against the new spec automatically. No human transcription. No stale payloads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: The Local Feedback Loop
&lt;/h2&gt;

&lt;p&gt;The workflow only holds if it becomes the standard operating loop before any push. The pattern is simple. Write the code. Run REST Assured locally from the IDE, which takes around five seconds. If the test fails, stay on the machine, open Postman to visually inspect the response against the live local server, fix the underlying code, and re-run. If the test passes, push the PR with confidence.&lt;/p&gt;

&lt;p&gt;The pipeline never sees the broken state. That is the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomy of a Catch: The Pagination Index Bug
&lt;/h2&gt;

&lt;p&gt;This is a real scenario that slips past traditional code review more often than it should.&lt;/p&gt;

&lt;p&gt;The API is designed with a 1-based pagination index. &lt;code&gt;page=1&lt;/code&gt; returns the first page of results. During a refactor, a developer accidentally shifts the implementation to 0-based. The code compiles. The database queries run. The application starts cleanly. Nothing in the build output signals a problem. In a siloed workflow, this ships, and downstream clients break on deploy.&lt;/p&gt;

&lt;p&gt;Here is how the dual-layer workflow catches it before that happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: The Automated Gate Stops the Push
&lt;/h3&gt;

&lt;p&gt;The REST Assured contract test for pagination is already in the suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;verifyPaginationContract&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;RestAssured&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;given&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queryParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"page"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queryParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/v1/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"currentPage"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hamcrest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Matchers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equalTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users.size()"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hamcrest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Matchers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;greaterThan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer runs this locally. Because the code now treats &lt;code&gt;page=1&lt;/code&gt; as the second page, the API returns an empty list and &lt;code&gt;currentPage: 2&lt;/code&gt;. The assertion fails in under five seconds. The branch stays local. The PR does not open.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Postman Makes the Bug Visible
&lt;/h3&gt;

&lt;p&gt;The developer imports the current &lt;code&gt;/v3/api-docs&lt;/code&gt; into Postman, fires a manual request with &lt;code&gt;page=1&lt;/code&gt;, and reads the raw response:&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;"currentPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"users"&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;The defect is immediately visible. No stack trace reading. No guesswork. Passing &lt;code&gt;page=1&lt;/code&gt; yields &lt;code&gt;currentPage: 2&lt;/code&gt;. The index shift is confirmed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Resolution Decision
&lt;/h3&gt;

&lt;p&gt;At this point the developer makes an explicit, documented decision rather than a silent one.&lt;/p&gt;

&lt;p&gt;If it was an accidental bug, fix the index mapping back to 1-based in the controller, re-run the REST Assured test until it is green, and push clean code. The pipeline never sees the regression.&lt;/p&gt;

&lt;p&gt;If it was an intentional requirement change, update the REST Assured assertion to &lt;code&gt;equalTo(0)&lt;/code&gt;, refresh the Postman collection to align, and commit the code and test change together in the same PR. The change is visible, traceable, and reviewed.&lt;/p&gt;

&lt;p&gt;The critical difference from the broken workflow is that the decision cannot happen silently. It is forced into the open at the developer's desk, not discovered in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Gives You
&lt;/h2&gt;

&lt;p&gt;REST Assured becomes an objective, automated gate. It does not care about developer intent. It verifies the contract and reports pass or fail.&lt;/p&gt;

&lt;p&gt;Postman stays a flexible, interactive debugging surface. It is no longer a documentation artifact that drifts. It is a live mirror of the running code.&lt;/p&gt;

&lt;p&gt;Both tools operate from the same definitions, derived in real time from the active application. There is no synchronization step to forget, and there is no version of the spec living outside the codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;This loop works cleanly on a single developer's machine. The feedback is fast, the gate is reliable, and the contract is grounded in the actual code.&lt;/p&gt;

&lt;p&gt;But what happens when 50 developers are all pushing to the same repository? What stops one of them from simply updating the REST Assured assertion to match their bug and pushing a green build?&lt;/p&gt;

&lt;p&gt;That is the governance problem. That is exactly what Part 2 covers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/prasadmk/who-approved-this-change-managing-api-contracts-and-test-rot-in-large-engineering-teams-39"&gt;Part 2: Who Approved This Change? Managing API Contracts and Test Rot in Large Engineering Teams&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Zero-Drift API Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-zero-drift-api-series-stop-trusting-a-green-build-you-cant-explain-k56"&gt;Intro: The Zero-Drift API Series: Stop Trusting a Green Build You Can't Explain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Part 1: A Guide to Stop Breaking Merges: Unifying Postman and REST Assured in Spring Boot&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/who-approved-this-change-managing-api-contracts-and-test-rot-in-large-engineering-teams-39"&gt;Part 2: Who Approved This Change? Managing API Contracts and Test Rot in Large Engineering Teams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-ai-superpower-how-vibe-coders-use-openapi-as-a-semantic-anchor-45kg"&gt;Part 3: The AI Superpower: How Vibe Coders Use OpenAPI as a Semantic Anchor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/locking-down-the-pipeline-enforcing-contract-integrity-against-autonomous-ai-agents-m5m"&gt;Part 4: Locking Down the Pipeline: Enforcing Contract Integrity Against Autonomous AI Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>java</category>
      <category>springboot</category>
      <category>testing</category>
    </item>
    <item>
      <title>The Zero-Drift API Series: Stop Trusting a Green Build You Can't Explain</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Sat, 30 May 2026 17:20:26 +0000</pubDate>
      <link>https://dev.to/prasadmk/the-zero-drift-api-series-stop-trusting-a-green-build-you-cant-explain-k56</link>
      <guid>https://dev.to/prasadmk/the-zero-drift-api-series-stop-trusting-a-green-build-you-cant-explain-k56</guid>
      <description>&lt;p&gt;There is a specific kind of production incident that hurts more than the others.&lt;/p&gt;

&lt;p&gt;Not the kind where the stack trace is obvious. The kind where the build was green, the tests passed, and the code review looked clean, and yet something that &lt;em&gt;used to work&lt;/em&gt; silently stopped working for a downstream team, a frontend client, or a mobile app. No alarm. No contract violation flagged. Just a broken assumption that traveled all the way to production dressed as a passing test.&lt;/p&gt;

&lt;p&gt;That is the &lt;strong&gt;drift problem&lt;/strong&gt;. And it is not a testing problem. It is a governance problem.&lt;/p&gt;

&lt;p&gt;This four-part series is a practical engineering framework for teams running Spring Boot REST APIs who want deterministic confidence, not just green builds, across every layer of their delivery pipeline: from a solo developer's local machine, up through a large distributed team, through AI-assisted development, and all the way to autonomous AI agents writing and merging code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Are Solving
&lt;/h2&gt;

&lt;p&gt;The root cause is structural, not cultural. When &lt;strong&gt;Postman lives in one silo and REST Assured lives in another&lt;/strong&gt;, teams get two independent descriptions of the same API that drift apart over time. The automated tests stop reflecting reality. The manual tests stop reflecting the code. And the first person to notice is usually a downstream consumer, in production.&lt;/p&gt;

&lt;p&gt;Layered on top of that: &lt;strong&gt;anyone can rewrite a failing test to make it pass&lt;/strong&gt;. A pagination index shifts from 1-based to 0-based, the assertion gets quietly updated to match, CI goes green, and three downstream clients break on the next deploy. The test did not catch the regression. The test &lt;em&gt;became&lt;/em&gt; the regression.&lt;/p&gt;

&lt;p&gt;Scale that to 50+ developers, add AI code generation tools that hallucinate payload keys and rewrite tests to cover their own mistakes, then add autonomous AI agents triggering pull requests, and the problem compounds fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four-Part Framework
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part 1: A Guide to Stop Breaking Merges: Unifying Postman and REST Assured in Spring Boot&lt;/strong&gt;&lt;br&gt;
The foundation. Establishing the Spring Boot application itself as the single source of truth via its live OpenAPI spec, so Postman and REST Assured consume identical definitions, eliminating drift at the individual developer loop before a line of code reaches the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2: Who Approved This Change? Managing API Contracts and Test Rot in Large Engineering Teams&lt;/strong&gt;&lt;br&gt;
The governance layer. What happens when 50+ developers are all touching the same codebase and one intentional change silently redefines a contract for everyone else. API versioning, &lt;code&gt;CODEOWNERS&lt;/code&gt;, and the architectural choice between "never touch old tests" (which causes test rot) and "freely modify tests" (which kills downstream trust).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3: The AI Superpower: How Vibe Coders Use OpenAPI as a Semantic Anchor&lt;/strong&gt;&lt;br&gt;
The AI-assisted development layer. How developers using Cursor, Copilot, or LLMs can ground their AI tools in the live local spec, eliminating hallucinated payload keys, automating test generation, and closing the feedback loop when AI-generated code silently breaks a contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4: Locking Down the Pipeline: Enforcing Contract Integrity Against Autonomous AI Agents&lt;/strong&gt;&lt;br&gt;
The enforcement layer. When AI agents are autonomously writing code and opening PRs, you cannot rely on them remembering rules. Pre-commit hooks, &lt;code&gt;.ai-rules.json&lt;/code&gt; constraint files, and a Coder/Auditor multi-agent pattern that treats the AI like an untrusted contributor, with deterministic, programmatic rails.&lt;/p&gt;




&lt;p&gt;Each part builds on the last. You can apply Part 1 today, in isolation. Parts 2 through 4 progressively harden that foundation for teams at scale.&lt;/p&gt;

&lt;p&gt;The goal throughout is the same: &lt;strong&gt;the build should be green because the contract is intact, not because someone updated the assertion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's start local.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/prasadmk/a-guide-to-stop-breaking-merges-unifying-postman-and-rest-assured-in-spring-boot-42fg"&gt;Part 1 -&amp;gt; A Guide to Stop Breaking Merges: Unifying Postman and REST Assured in Spring Boot&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Zero-Drift API Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-zero-drift-api-series-stop-trusting-a-green-build-you-cant-explain-k56"&gt;Intro: The Zero-Drift API Series: Stop Trusting a Green Build You Can't Explain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/a-guide-to-stop-breaking-merges-unifying-postman-and-rest-assured-in-spring-boot-42fg"&gt;Part 1: A Guide to Stop Breaking Merges: Unifying Postman and REST Assured in Spring Boot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/who-approved-this-change-managing-api-contracts-and-test-rot-in-large-engineering-teams-39"&gt;Part 2: Who Approved This Change? Managing API Contracts and Test Rot in Large Engineering Teams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/the-ai-superpower-how-vibe-coders-use-openapi-as-a-semantic-anchor-45kg"&gt;Part 3: The AI Superpower: How Vibe Coders Use OpenAPI as a Semantic Anchor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/prasadmk/locking-down-the-pipeline-enforcing-contract-integrity-against-autonomous-ai-agents-m5m"&gt;Part 4: Locking Down the Pipeline: Enforcing Contract Integrity Against Autonomous AI Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vibecoding</category>
      <category>testing</category>
      <category>agents</category>
      <category>java</category>
    </item>
  </channel>
</rss>
