<?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>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>
    <item>
      <title>Rescuing a Broken Master from a Tag, Cleanly Through a PR</title>
      <dc:creator>Prasad MK</dc:creator>
      <pubDate>Fri, 29 May 2026 06:46:14 +0000</pubDate>
      <link>https://dev.to/prasadmk/rescuing-a-broken-master-from-a-tag-cleanly-through-a-pr-416i</link>
      <guid>https://dev.to/prasadmk/rescuing-a-broken-master-from-a-tag-cleanly-through-a-pr-416i</guid>
      <description>&lt;p&gt;It happens on real teams. Someone merges the wrong thing, a dependency update silently breaks the build, or a hotfix lands without a proper review. Whatever the reason, &lt;strong&gt;master is broken and you still have work to ship.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The good news: if you have a working release tag, you have everything you need. This guide walks through the full recovery arc, branching from the tag, adding new code on top, and raising a clean pull request that merges back to master with no surprise conflicts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why branching from the tag is the right move
&lt;/h2&gt;

&lt;p&gt;The instinct is often to "fix master directly." Resist it. You do not know the full extent of what broke, and working on master violates the PR policy anyway. The tag is a precise snapshot of code that worked. Treating it as your new baseline keeps every subsequent decision clean and reviewable.&lt;/p&gt;

&lt;p&gt;The strategy has three phases: &lt;strong&gt;establish&lt;/strong&gt; a branch at the good commit, &lt;strong&gt;build&lt;/strong&gt; your new work on top of it, then &lt;strong&gt;reconcile&lt;/strong&gt; the divergence before raising a PR so the reviewer sees nothing but your intentional changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Create a branch from the tag
&lt;/h2&gt;

&lt;p&gt;This single command is the foundation of everything that follows. It tells Git to set your new branch's starting commit to exactly where &lt;code&gt;v1.3&lt;/code&gt; points, not to the current tip of master.&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;# Create the branch rooted at the v1.3 tag&lt;/span&gt;
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; fix/restore-from-v1.3 v1.3

&lt;span class="c"&gt;# Push it to Bitbucket so the team can see it&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin fix/restore-from-v1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before writing a single line of new code, take a moment to understand exactly what diverged. This pays off when you reconcile later.&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;# What commits exist on master that are not in v1.3?&lt;/span&gt;
git log v1.3..origin/master &lt;span class="nt"&gt;--oneline&lt;/span&gt;

&lt;span class="c"&gt;# What do those changes actually look like?&lt;/span&gt;
git diff v1.3 origin/master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; The diff above tells you exactly which files the broken commits touched. Those are the files you will need to consciously handle when you reconcile later. Keep a mental note of them.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 2: Add your new work on top
&lt;/h2&gt;

&lt;p&gt;You are now on a stable foundation. Build your features or fixes as normal, with one discipline: commit small and commit often. This is not just good practice in general, it becomes essential when you need to explain your PR to a reviewer who is looking at a branch that diverges from a broken master.&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;# Work on your files normally&lt;/span&gt;
git add src/feature-x.js src/utils.js

&lt;span class="c"&gt;# Write commit messages that will read well in the PR&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add validation layer for incoming payloads"&lt;/span&gt;

git add tests/feature-x.test.js
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"test: unit coverage for payload validator"&lt;/span&gt;

&lt;span class="c"&gt;# Push regularly : this keeps Bitbucket in sync&lt;/span&gt;
git push origin fix/restore-from-v1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Phase 3: Reconcile before you raise the PR
&lt;/h2&gt;

&lt;p&gt;This is the step most guides skip or handle lazily. If you raise a PR now, Bitbucket will show conflicts because your branch started at &lt;code&gt;v1.3&lt;/code&gt; and master has moved (to broken commits) since then. The reviewer gets a noisy, confusing diff.&lt;/p&gt;

&lt;p&gt;The correct approach is to resolve all of this &lt;strong&gt;locally on your branch&lt;/strong&gt; before the PR goes up. Bitbucket then shows a clean diff of exactly what you intended to change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Pull master's current state into your branch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Make sure you have the latest remote state&lt;/span&gt;
git fetch origin

&lt;span class="c"&gt;# While ON your fix branch, merge master in&lt;/span&gt;
git checkout fix/restore-from-v1.3
git merge origin/master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Resolve conflicts, keeping your code
&lt;/h3&gt;

&lt;p&gt;Git will flag every file where your branch and the broken master diverge. For each conflict, you decide: keep your code, keep master's code, or blend them. In a recovery scenario, you almost always want to keep your branch's version.&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;# Option A: Accept your branch's version for a specific file&lt;/span&gt;
git checkout &lt;span class="nt"&gt;--ours&lt;/span&gt; src/api/handler.js
git add src/api/handler.js

&lt;span class="c"&gt;# Option B: Open the file manually, remove conflict markers,&lt;/span&gt;
&lt;span class="c"&gt;# edit to the state you want, then stage it&lt;/span&gt;
&lt;span class="c"&gt;# &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD  (your branch)&lt;/span&gt;
&lt;span class="c"&gt;# =======       (divider)&lt;/span&gt;
&lt;span class="c"&gt;# &amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; origin/master  (master)&lt;/span&gt;
git add src/api/handler.js

&lt;span class="c"&gt;# Once all conflicts are resolved, commit the merge&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"merge: absorb origin/master, keep v1.3 baseline + new features"&lt;/span&gt;

&lt;span class="c"&gt;# Push the resolved branch&lt;/span&gt;
git push origin fix/restore-from-v1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Watch out with &lt;code&gt;--ours&lt;/code&gt;:&lt;/strong&gt; During a &lt;code&gt;git merge&lt;/code&gt;, "ours" refers to the branch you were on when you ran the merge your fix branch. "Theirs" refers to the branch being merged in master. This is the &lt;strong&gt;opposite&lt;/strong&gt; of how it works during a &lt;code&gt;git rebase&lt;/code&gt;. Keep this straight.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3: Verify the diff before raising the PR
&lt;/h3&gt;

&lt;p&gt;This is your sanity check. The three-dot diff shows only the commits that exist on your branch but not on master, exactly what the reviewer will see in the PR.&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;# Three-dot diff: "what does my branch add beyond master?"&lt;/span&gt;
git diff origin/master...fix/restore-from-v1.3

&lt;span class="c"&gt;# Cross-check: should show ONLY your new intentional changes&lt;/span&gt;
git diff v1.3 HEAD

&lt;span class="c"&gt;# If the output matches your intentions, you are ready to raise the PR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Raising the pull request in Bitbucket
&lt;/h2&gt;

&lt;p&gt;With the branch pushed and conflicts pre-resolved, the PR creation is straightforward. In Bitbucket, navigate to your repository, go to &lt;strong&gt;Pull Requests&lt;/strong&gt;, and create a new PR from &lt;code&gt;fix/restore-from-v1.3&lt;/code&gt; to &lt;code&gt;master&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Source branch:&lt;/strong&gt; &lt;code&gt;fix/restore-from-v1.3&lt;/code&gt;. Target branch: &lt;code&gt;master&lt;/code&gt;. Bitbucket should now show no merge conflicts because you resolved them on your branch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write a clear PR description.&lt;/strong&gt; Mention that the branch was rooted at tag &lt;code&gt;v1.3&lt;/code&gt; due to the broken master state. Reviewers need this context to evaluate the diff correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Link the relevant issue or Jira ticket&lt;/strong&gt; if your team uses one. This anchors the PR to business context and keeps the audit trail intact.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The reviewer sees a clean, intentional diff.&lt;/strong&gt; No broken master code, no spurious conflict markers. Just your new work on top of the last stable release.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What happens to master after the merge
&lt;/h2&gt;

&lt;p&gt;When the PR merges, master will contain: the complete history up to &lt;code&gt;v1.3&lt;/code&gt;, the merge commit that absorbed the broken state, and your new feature commits on top. The broken commits are still in the history, they are not erased, but they are effectively neutralized because your branch's code takes precedence in the final snapshot.&lt;/p&gt;

&lt;p&gt;This is the cleanest outcome achievable under a PR-only policy. You did not force-push, you did not rewrite shared history, and every change that landed in master went through a review.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on force-pushing master:&lt;/strong&gt; Some guides suggest resetting master to the tag and force-pushing. That works in a solo repo but is destructive on a shared branch, it rewrites history that other developers may have already pulled. The approach in this guide avoids that entirely.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Quick reference: the full command sequence
&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;# 1. Branch from the working tag&lt;/span&gt;
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; fix/restore-from-v1.3 v1.3
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin fix/restore-from-v1.3

&lt;span class="c"&gt;# 2. Understand the divergence (read-only, safe to run anytime)&lt;/span&gt;
git log v1.3..origin/master &lt;span class="nt"&gt;--oneline&lt;/span&gt;
git diff v1.3 origin/master

&lt;span class="c"&gt;# 3. Do your work, commit often&lt;/span&gt;
git add &amp;lt;files&amp;gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: your message here"&lt;/span&gt;
git push origin fix/restore-from-v1.3

&lt;span class="c"&gt;# 4. Absorb master into your branch (pre-resolve before PR)&lt;/span&gt;
git fetch origin
git merge origin/master
&lt;span class="c"&gt;# resolve conflicts, then:&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"merge: absorb master, keep v1.3 baseline + new features"&lt;/span&gt;
git push origin fix/restore-from-v1.3

&lt;span class="c"&gt;# 5. Verify the diff is clean&lt;/span&gt;
git diff origin/master...fix/restore-from-v1.3

&lt;span class="c"&gt;# 6. Raise the PR in Bitbucket: fix/restore-from-v1.3 -&amp;gt; master&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The principle worth keeping
&lt;/h2&gt;

&lt;p&gt;Tags are not just release markers. They are &lt;strong&gt;named, immutable recovery points&lt;/strong&gt;. Any time master becomes unstable and a working tag exists, you have a clean path back, without rewriting shared history, without bypassing your PR policy, and without making your reviewer's job harder than it needs to be. The work is in the reconciliation step. Get that right, and the merge is uneventful.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>git</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
