<?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: Gabriel Anhaia</title>
    <description>The latest articles on DEV Community by Gabriel Anhaia (@gabrielanhaia).</description>
    <link>https://dev.to/gabrielanhaia</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F425693%2Fb4e58b6f-fdb0-4d1d-b333-87582f0b663d.jpeg</url>
      <title>DEV Community: Gabriel Anhaia</title>
      <link>https://dev.to/gabrielanhaia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gabrielanhaia"/>
    <language>en</language>
    <item>
      <title>Stop Passing *sql.Tx Through Your Go Service Layer</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:53:55 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/stop-passing-sqltx-through-your-go-service-layer-lp7</link>
      <guid>https://dev.to/gabrielanhaia/stop-passing-sqltx-through-your-go-service-layer-lp7</guid>
      <description>&lt;p&gt;Your service needs to save an order and reserve inventory. Both must succeed or neither should. The instinct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PlaceOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SaveWithTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventoryRepo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReserveWithTx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles. It works. &lt;strong&gt;And it destroys your architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Wrong
&lt;/h2&gt;

&lt;p&gt;Your domain now imports &lt;code&gt;database/sql&lt;/code&gt;. The service knows what a SQL transaction is. Every port needs a "WithTx" variant. Your in-memory test doubles are useless — they can't accept &lt;code&gt;*sql.Tx&lt;/code&gt;. And if you ever move to DynamoDB? Its transactional model is completely different. You're locked in.&lt;/p&gt;

&lt;p&gt;The domain should express &lt;strong&gt;what&lt;/strong&gt; needs to happen atomically. The adapter should decide &lt;strong&gt;how&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unit of Work Pattern
&lt;/h2&gt;

&lt;p&gt;Define a port that says "execute these operations as one atomic unit" without specifying the mechanism:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderSaver&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SaveOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;InventoryReserver&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ReserveInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;productID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UnitOfWorkTx&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OrderSaver&lt;/span&gt;
    &lt;span class="n"&gt;InventoryReserver&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UnitOfWork&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="n"&gt;UnitOfWorkTx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The domain calls &lt;code&gt;Execute&lt;/code&gt; with a function. Inside, it uses the &lt;code&gt;tx&lt;/code&gt; to save and reserve. If the function returns nil, everything commits. If it returns an error, everything rolls back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PlaceOrderService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;uow&lt;/span&gt;   &lt;span class="n"&gt;UnitOfWork&lt;/span&gt;
    &lt;span class="n"&gt;idGen&lt;/span&gt; &lt;span class="n"&gt;IDGenerator&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PlaceOrderService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;PlaceOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;LineItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idGen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;CustomerID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="n"&gt;UnitOfWorkTx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SaveOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"saving order: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReserveInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"reserving inventory: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count the imports. &lt;code&gt;context&lt;/code&gt;, &lt;code&gt;fmt&lt;/code&gt;. No &lt;code&gt;database/sql&lt;/code&gt;. The service has no idea whether "atomically" means a SQL transaction, a DynamoDB transact-write, or an in-memory buffer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The In-Memory Adapter (For Tests)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;InMemoryUoW&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;        &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt;    &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;
    &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;InMemoryUoW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="n"&gt;UnitOfWorkTx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// Buffer changes&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;inMemoryTx&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// discard buffer = rollback&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Commit: apply buffered changes to real state&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the function fails, the buffer is discarded. The real state never changes. That's rollback — in pure Go, with zero SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rollback Test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestPlaceOrder_RollbackOnFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;uow&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewInMemoryUoW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewPlaceOrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;seqIDGen&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PlaceOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cust-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;LineItem&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ProductID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"prod-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ProductID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"prod-b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c"&gt;// invalid — will fail&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"expected error"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Nothing committed&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders should be empty after rollback"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"inventory should be empty after rollback"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order was saved inside the function, but the reservation failed. Both were rolled back. The test proves it — in microseconds, with no database.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You Don't Need This
&lt;/h2&gt;

&lt;p&gt;Not every service needs Unit of Work. If your operation makes one database call, a simple repository port is enough. UoW adds complexity. Use it when you genuinely need atomic operations across multiple repositories.&lt;/p&gt;




&lt;p&gt;📖 This is Chapter 16 of &lt;strong&gt;&lt;a href="https://www.amazon.com/dp/B0GGVBZ28S" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go: Ports, Adapters, and Services That Last&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;The book covers the full journey: from the spaghetti service that breaks every sprint, through domain modeling, port design, adapter building, and production patterns like this one — to knowing when hexagonal architecture is overkill and skipping it confidently.&lt;/p&gt;

&lt;p&gt;22 chapters. Every example tested. &lt;a href="https://github.com/gabrielanhaia/hexagonal-go-examples" rel="noopener noreferrer"&gt;Companion repo included&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Book 2 in the &lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;Thinking in Go&lt;/a&gt; series.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 5 of 5. Thanks for reading the series. If it helped, the book goes 10x deeper on every topic.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>architecture</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Dependency Rule: One Import Statement Will Tell You If Your Go Architecture Is Broken</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:53:47 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/the-dependency-rule-one-import-statement-will-tell-you-if-your-go-architecture-is-broken-4c4d</link>
      <guid>https://dev.to/gabrielanhaia/the-dependency-rule-one-import-statement-will-tell-you-if-your-go-architecture-is-broken-4c4d</guid>
      <description>&lt;p&gt;Open your domain package. Read the imports.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. This domain depends on nothing but standard library utilities.&lt;/p&gt;

&lt;p&gt;Now look at this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"database/sql"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One extra line. &lt;code&gt;database/sql&lt;/code&gt;. Your domain now depends on infrastructure. The architecture is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Inner layers must never import outer layers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The domain defines interfaces. The adapters implement them. The domain never knows which adapter is on the other side. That's the entire rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Adapter ──imports──► Domain    ✅ Correct
  Domain  ──imports──► Adapter   ❌ Broken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What "Depends On" Means in Go
&lt;/h2&gt;

&lt;p&gt;In Go, dependency is an &lt;code&gt;import&lt;/code&gt;. Nothing else. No annotation-based DI. No classpath scanning. If package A imports package B, A depends on B.&lt;/p&gt;

&lt;p&gt;Want to know which direction your dependencies point? Open the file. Read the imports. The compiler is your architecture linter.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Violations Sneak In
&lt;/h2&gt;

&lt;p&gt;Even experienced developers break the rule accidentally:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON struct tags on domain types.&lt;/strong&gt; You add &lt;code&gt;json:"customer_id"&lt;/code&gt; to your &lt;code&gt;Order&lt;/code&gt; struct. Now your domain knows about serialization. That knowledge belongs in the adapter's DTO types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sql.NullString&lt;/code&gt; in a domain field.&lt;/strong&gt; "It's just one field." Now your domain imports &lt;code&gt;database/sql&lt;/code&gt;. Every test needs the sql package. You're locked to SQL databases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;*http.Request&lt;/code&gt; as a domain method parameter.&lt;/strong&gt; Your domain now depends on &lt;code&gt;net/http&lt;/code&gt;. Can't call that method from a CLI tool or a Kafka consumer without importing the HTTP package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ORM tags.&lt;/strong&gt; &lt;code&gt;gorm:"column:order_id"&lt;/code&gt; on your domain struct. Your domain depends on &lt;code&gt;gorm.io&lt;/code&gt;. If you switch ORMs, you rewrite the domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Returning &lt;code&gt;pq.Error&lt;/code&gt; from an adapter.&lt;/strong&gt; The raw PostgreSQL error escapes into the service layer. Anyone handling it must import &lt;code&gt;github.com/lib/pq&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each seems harmless. Together they erode the boundary until your "hexagonal architecture" is just a folder structure with no enforcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Correct Way
&lt;/h2&gt;

&lt;p&gt;The domain defines a port. The adapter implements it. The domain never imports the adapter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// domain/service.go — imports: context, fmt. Nothing else.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"customer ID is required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;generateID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;CustomerID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TotalCents&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"saving order: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// adapter/memory/repository.go — imports the domain, not the other way around&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;InMemoryRepository&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;InMemoryRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter knows about the domain. The domain has no idea the adapter exists. Arrow points inward.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Detect Violations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read the imports.&lt;/strong&gt; Open your domain package. If you see &lt;code&gt;database/sql&lt;/code&gt;, &lt;code&gt;net/http&lt;/code&gt;, &lt;code&gt;encoding/json&lt;/code&gt;, or any ORM, fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile the domain alone:&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;go build ./internal/domain/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fails because of missing infrastructure packages, you have a violation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automate it:&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;go list &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.Imports}}'&lt;/span&gt; ./internal/domain/... | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"database|net/http|encoding/json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns anything, the architecture has a hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;main()&lt;/code&gt; Is the Exception
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;main()&lt;/code&gt; imports everything. It creates adapters, injects them into services, and starts the server. That's its job — to be the one place where all layers meet so that nothing else has to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httphandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;main()&lt;/code&gt; is the composition root, not part of any layer.&lt;/p&gt;




&lt;p&gt;📖 The dependency rule is Chapter 7 of &lt;strong&gt;&lt;a href="https://www.amazon.com/dp/B0GGVBZ28S" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;&lt;/strong&gt;, and it's the foundation everything else builds on. The book covers correct vs incorrect dependency direction with side-by-side code, how violations sneak in through five common patterns, and detection techniques including automated import checking.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Part 4 of 5. Final post: stop passing &lt;code&gt;*sql.Tx&lt;/code&gt; through your service layer — the Unit of Work pattern in Go.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>architecture</category>
      <category>cleancode</category>
      <category>programming</category>
    </item>
    <item>
      <title>Testing a Go Service in Microseconds: The Hexagonal Testing Strategy</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:53:42 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/testing-a-go-service-in-microseconds-the-hexagonal-testing-strategy-198p</link>
      <guid>https://dev.to/gabrielanhaia/testing-a-go-service-in-microseconds-the-hexagonal-testing-strategy-198p</guid>
      <description>&lt;p&gt;Your Go tests take 3 minutes because every "unit test" spins up PostgreSQL in Docker.&lt;/p&gt;

&lt;p&gt;That's not a testing problem. That's an architecture problem.&lt;/p&gt;

&lt;p&gt;When your business logic is tangled with your database calls, the only way to test it is to bring the database along. Hexagonal architecture untangles them — and testing becomes almost mechanical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Layers, Three Strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1: Domain Tests — Microseconds
&lt;/h3&gt;

&lt;p&gt;The domain is pure Go. No imports from &lt;code&gt;database/sql&lt;/code&gt;, &lt;code&gt;net/http&lt;/code&gt;, or any external package. Testing it is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestNewOrder_RejectsEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ord-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cust-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrOrderEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"got %v, want ErrOrderEmpty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestOrder_Confirm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;validOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Confirm&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unexpected error: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;OrderStatusConfirmed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status = %q, want confirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No setup. No teardown. No containers. These tests finish before your terminal redraws.&lt;/p&gt;

&lt;p&gt;For the service layer, test doubles are trivial because the ports are small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// 8 lines. That's the entire repository double.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;inMemoryRepo&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inMemoryRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inMemoryRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;ErrOrderNotFound&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 2: Adapter Tests — Milliseconds
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;HTTP handlers:&lt;/strong&gt; Use &lt;code&gt;httptest&lt;/code&gt;. Stub the domain behind the port interface. Test that JSON→domain translation works and errors→status codes map correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestCreateOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;stubService&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httphandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;`{"customer_id":"cust-1","items":[{"product_id":"p1","quantity":2,"price_cents":2500}]}`&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httptest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httptest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRecorder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeHTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCreated&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status = %d, want 201"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No real service. No database. You're testing &lt;em&gt;translation&lt;/em&gt;, not business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database adapters:&lt;/strong&gt; For in-memory adapters, test directly. For PostgreSQL, use integration tests (testcontainers) — but only to verify the adapter implements the port correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Integration Tests — Seconds
&lt;/h3&gt;

&lt;p&gt;Full stack through HTTP. Build real adapters, wire through the service, send HTTP requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestFullStack_CreateAndGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOrderRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;noopNotifier&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;seqIDGen&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httphandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;srv&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httptest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// Create&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`{"customer_id":"cust-1","items":[...]}`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c"&gt;// assert 201...&lt;/span&gt;

    &lt;span class="c"&gt;// Get&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/orders/ord-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// assert 200 + correct body...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use sparingly. These catch wiring bugs, not business logic bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pyramid
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        /\
       /  \        Few integration tests (slow, catch wiring bugs)
      /----\
     /      \      Some adapter tests (medium, catch translation bugs)
    /--------\
   /          \    Many domain tests (fast, catch business logic bugs)
  /____________\
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pyramid falls naturally out of the architecture. You don't need a testing strategy document. The architecture IS the testing strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hand-Written Doubles &amp;gt; Mock Frameworks
&lt;/h2&gt;

&lt;p&gt;With 1-2 method interfaces, a hand-written stub is 5-10 lines. A mocking framework adds code generation, magic assertions, and runtime reflection. The framework overhead isn't worth it for interfaces this small.&lt;/p&gt;

&lt;p&gt;Write the struct. Implement the methods. Move on.&lt;/p&gt;




&lt;p&gt;📖 This is the testing strategy from Chapter 14 of &lt;strong&gt;&lt;a href="https://www.amazon.com/dp/B0GGVBZ28S" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;&lt;/strong&gt;. The book also covers conformance tests (run the same suite against every adapter), error mapping tests, and when integration tests are genuinely necessary.&lt;/p&gt;

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

&lt;p&gt;Companion code: &lt;a href="https://github.com/gabrielanhaia/hexagonal-go-examples" rel="noopener noreferrer"&gt;github.com/gabrielanhaia/hexagonal-go-examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Part 3 of 5. Next: the one rule that holds everything together — the dependency rule, and what breaks when you violate it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Go Interfaces Are Ports: The Language Feature That Makes Clean Architecture Free</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:53:38 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/go-interfaces-are-ports-the-language-feature-that-makes-clean-architecture-free-2pol</link>
      <guid>https://dev.to/gabrielanhaia/go-interfaces-are-ports-the-language-feature-that-makes-clean-architecture-free-2pol</guid>
      <description>&lt;p&gt;The biggest insight that changed how I structure Go services:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define interfaces where you USE them, not where you implement them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one idea — consumer-defined interfaces — is what makes hexagonal architecture feel native in Go instead of bolted on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Java Way vs The Go Way
&lt;/h2&gt;

&lt;p&gt;In Java, the class that implements an interface must explicitly declare it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostgresOrderRepository&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Must import OrderRepository. Must name it. Coupled at the declaration.&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Go, a type satisfies an interface just by having the right methods. No keyword. No import.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Domain package defines what it needs&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Adapter package — doesn't even need to know the interface exists&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PostgresOrderRepository&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PostgresOrderRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// SQL INSERT&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PostgresOrderRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// SQL SELECT&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;nil&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;PostgresOrderRepository&lt;/code&gt; never mentions &lt;code&gt;OrderRepository&lt;/code&gt;. The dependency arrow points &lt;strong&gt;inward&lt;/strong&gt; — from infrastructure to domain — exactly as hexagonal architecture demands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Interfaces Win
&lt;/h2&gt;

&lt;p&gt;The Go proverb: &lt;strong&gt;"The bigger the interface, the weaker the abstraction."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// 12 methods. Every test double implements all 12.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;FindByCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customerID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="c"&gt;// ... six more methods nobody uses together&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderSaver&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderFinder&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Compose when a service genuinely needs both&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;OrderSaver&lt;/span&gt;
    &lt;span class="n"&gt;OrderFinder&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a service that only writes depends on &lt;code&gt;OrderSaver&lt;/code&gt;. Test double: 5 lines. A service that only reads depends on &lt;code&gt;OrderFinder&lt;/code&gt;. Test double: 5 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Testing Payoff
&lt;/h2&gt;

&lt;p&gt;With small, consumer-defined interfaces, test doubles are trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;inMemoryRepo&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inMemoryRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;inMemoryRepo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;ErrOrderNotFound&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No mocking framework. No code generation. Your domain tests run in microseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestPlaceOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;inMemoryRepo&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewOrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PlaceOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// assert...&lt;/span&gt;

    &lt;span class="c"&gt;// Verify it was saved&lt;/span&gt;
    &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// assert...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No database. No Docker. No setup. Just Go structs and method calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rule
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Define interfaces in the &lt;strong&gt;consumer&lt;/strong&gt; package (the domain)&lt;/li&gt;
&lt;li&gt;Keep them &lt;strong&gt;small&lt;/strong&gt; (1-3 methods)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose&lt;/strong&gt; when needed via embedding&lt;/li&gt;
&lt;li&gt;Let Go's implicit satisfaction handle the wiring&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No framework needed. The language does the work.&lt;/p&gt;




&lt;p&gt;📖 This is Chapter 5 of my book &lt;strong&gt;&lt;a href="https://www.amazon.com/dp/B0GGVBZ28S" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go&lt;/a&gt;&lt;/strong&gt; — which goes deep into port design, naming conventions, when to split vs compose, and the IDGenerator pattern for deterministic testing.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Part 2 of 5 in the Hexagonal Architecture in Go series. Next: testing your hexagonal service at every layer — domain, adapters, and integration.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>architecture</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Hexagonal Architecture in Go: Why Your Service's Business Logic Should Know Nothing About HTTP</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:53:26 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/hexagonal-architecture-in-go-why-your-services-business-logic-should-know-nothing-about-http-3419</link>
      <guid>https://dev.to/gabrielanhaia/hexagonal-architecture-in-go-why-your-services-business-logic-should-know-nothing-about-http-3419</guid>
      <description>&lt;p&gt;You've seen it happen. Maybe you caused it.&lt;/p&gt;

&lt;p&gt;A Go service starts as a clean &lt;code&gt;main.go&lt;/code&gt; with a few handlers. Six months later, the handler that was 40 lines is 400. The SQL queries live inside HTTP handlers. The business logic imports &lt;code&gt;database/sql&lt;/code&gt;. The test suite takes six minutes because every test needs a running PostgreSQL.&lt;/p&gt;

&lt;p&gt;Someone suggests a refactor. A Jira ticket gets created. Nobody assigns it.&lt;/p&gt;

&lt;p&gt;This isn't because Go developers are bad. It's because Go gives you freedom without structure. No mandatory base classes. No framework telling you where things go. Freedom without a plan leads to spaghetti.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One Rule That Fixes It
&lt;/h2&gt;

&lt;p&gt;Hexagonal architecture (ports and adapters) comes down to one rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependencies always point inward. The domain knows nothing about infrastructure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;Order&lt;/code&gt; entity doesn't know it's stored in PostgreSQL. Your &lt;code&gt;OrderService&lt;/code&gt; doesn't know it's called from an HTTP handler. Your discount calculation doesn't know the coupon code came from a JSON body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// The domain defines what it needs — a port (interface)&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// The domain service depends on the interface, not the database&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewOrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter (PostgreSQL, in-memory, whatever) implements the interface. The domain never knows which one is on the other side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go Is Perfect for This
&lt;/h2&gt;

&lt;p&gt;In Java, hexagonal architecture requires dependency injection frameworks, annotation processors, and configuration hell. In Go, you need &lt;strong&gt;interfaces and a &lt;code&gt;main()&lt;/code&gt; function&lt;/strong&gt;. That's it.&lt;/p&gt;

&lt;p&gt;Go's implicit interfaces are the killer feature. The adapter doesn't need to declare &lt;code&gt;implements OrderRepository&lt;/code&gt;. It just needs the right methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// This automatically satisfies OrderRepository&lt;/span&gt;
&lt;span class="c"&gt;// No "implements" keyword. No import of the domain package needed.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PostgresOrderRepository&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PostgresOrderRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// SQL INSERT&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PostgresOrderRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// SQL SELECT, translate sql.ErrNoRows to domain.ErrOrderNotFound&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;main()&lt;/code&gt; is your composition root — the one place where everything gets wired:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOrderRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewNotifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smtpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httphandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST /orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No framework. No reflection. No magic. Five lines of wiring and you can see the entire architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Before and After
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt; (spaghetti):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic mixed with HTTP parsing and SQL queries&lt;/li&gt;
&lt;li&gt;Can't test without a running database&lt;/li&gt;
&lt;li&gt;Can't add gRPC without copy-pasting the handler&lt;/li&gt;
&lt;li&gt;New developers take weeks to understand the codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt; (hexagonal):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain tests run in microseconds — no Docker, no database&lt;/li&gt;
&lt;li&gt;Swap PostgreSQL for DynamoDB by changing one line in &lt;code&gt;main()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add gRPC, CLI, or Kafka consumer without touching business logic&lt;/li&gt;
&lt;li&gt;New developers read &lt;code&gt;main()&lt;/code&gt; and understand the architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Book
&lt;/h2&gt;

&lt;p&gt;I wrote a full book on this: &lt;strong&gt;22 chapters, 5 parts, from spaghetti code to production-ready hexagonal services in Go.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;📖 &lt;strong&gt;&lt;a href="https://www.amazon.com/dp/B0GGVBZ28S" rel="noopener noreferrer"&gt;Hexagonal Architecture in Go: Ports, Adapters, and Services That Last&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It covers everything: domain modeling, port design, HTTP and outbound adapters, dependency injection in &lt;code&gt;main()&lt;/code&gt;, testing at every layer, error handling across boundaries, transactions (Unit of Work), event-driven adapters, observability via decorators, when to skip hexagonal entirely, and migrating existing services incrementally.&lt;/p&gt;

&lt;p&gt;Every code example is tested in a &lt;a href="https://github.com/gabrielanhaia/hexagonal-go-examples" rel="noopener noreferrer"&gt;companion repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Book 2 in the "Thinking in Go" series.&lt;/strong&gt; Book 1 (&lt;a href="https://xgabriel.com/go-book" rel="noopener noreferrer"&gt;The Complete Guide to Go Programming&lt;/a&gt;) teaches the language. Book 2 teaches how to architect with it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Part 1 of a 5-part series on Hexagonal Architecture in Go. Next up: how Go interfaces naturally become ports — and why that matters more than you think.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>architecture</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your Vulnerability Scanner Was the Vulnerability: 4 Projects Backdoored in 8 Days</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 05 Apr 2026 17:24:57 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/your-vulnerability-scanner-was-the-vulnerability-4-projects-backdoored-in-8-days-46kd</link>
      <guid>https://dev.to/gabrielanhaia/your-vulnerability-scanner-was-the-vulnerability-4-projects-backdoored-in-8-days-46kd</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;gabrielanhaia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Trivy is a vulnerability scanner. Its whole job is finding security problems in other people's code.&lt;/p&gt;

&lt;p&gt;On March 22, 2026, it became one.&lt;/p&gt;

&lt;p&gt;Not a metaphor. Not an exaggeration. A threat group tracked as TeamPCP (Mandiant designation UNC6780) compromised Trivy's GitHub Actions workflow, injected a credential-stealing payload called SANDCLOCK, and turned every pipeline running the affected action into a silent data exfiltration machine. SSH keys, API tokens, cloud credentials. Gone. Through the tool teams installed &lt;em&gt;specifically&lt;/em&gt; to stop that from happening.&lt;/p&gt;

&lt;p&gt;The punchline? Trivy wasn't even the only victim that week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight Days, Four Compromises, Two Security Scanners
&lt;/h2&gt;

&lt;p&gt;Here's the full timeline. Each entry includes the attack vector and affected versions based on published advisories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;March 19 — LiteLLM (PyPI)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LiteLLM provides a unified Python interface for calling multiple LLM providers. Thousands of AI projects depend on it. TeamPCP published a poisoned package to PyPI containing the SANDCLOCK payload embedded in the package's &lt;code&gt;setup.py&lt;/code&gt; post-install hook.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Affected versions:&lt;/strong&gt; &lt;code&gt;1.56.3&lt;/code&gt; through &lt;code&gt;1.56.5&lt;/code&gt; on PyPI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known-bad hash (1.56.4):&lt;/strong&gt; &lt;code&gt;sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean version:&lt;/strong&gt; &lt;code&gt;1.56.6&lt;/code&gt; (published March 23 after PyPI yanked the compromised releases)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anyone who ran &lt;code&gt;pip install litellm&lt;/code&gt; or let automated dependency updates fire during that four-day window pulled in malware alongside their LLM proxy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;March 21 — Telnyx Python SDK (PyPI)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same playbook, different target. Telnyx's SDK handles voice, messaging, and communications APIs. TeamPCP pushed a modified package with SANDCLOCK baked into the initialization module.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Affected versions:&lt;/strong&gt; &lt;code&gt;2.1.0&lt;/code&gt; through &lt;code&gt;2.1.2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known-bad hash (2.1.1):&lt;/strong&gt; &lt;code&gt;sha256:7d793037a0760186574b0282f2f435e7c0b4a3b5e38d25f9c1db4b79e5f1a2c0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean version:&lt;/strong&gt; &lt;code&gt;2.1.3&lt;/code&gt; (published March 24)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;March 22 — Trivy (GitHub Actions)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one stings differently. Aqua Security's Trivy is one of the most deployed container vulnerability scanners on the planet. It scans Docker images, filesystems, and Git repos for CVEs, misconfigurations, and exposed secrets. TeamPCP didn't bother with PyPI this time. They went straight for the GitHub Actions workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Affected action:&lt;/strong&gt; &lt;code&gt;aquasecurity/trivy-action&lt;/code&gt; with mutable tags &lt;code&gt;@v1&lt;/code&gt; and &lt;code&gt;@latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compromised commit SHA:&lt;/strong&gt; &lt;code&gt;b4b587a89b42c8b9b4494c2e3f58f5e33eb937bb&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean commit SHA:&lt;/strong&gt; &lt;code&gt;f1ef53dab1f0a26b0f9cda0f94e66e3f93ae6375&lt;/code&gt; (tag restored March 25)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposure window:&lt;/strong&gt; March 22 - March 25&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;March 27 — KICS (GitHub Actions)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The final strike. Checkmarx's KICS scans Terraform, CloudFormation, Ansible, and Kubernetes manifests for security misconfigurations. Same vector as Trivy. Same payload.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Affected action:&lt;/strong&gt; &lt;code&gt;checkmarx/kics-github-action&lt;/code&gt; with mutable tags &lt;code&gt;@v2&lt;/code&gt; and &lt;code&gt;@latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compromised commit SHA:&lt;/strong&gt; &lt;code&gt;94a2d2cfee7c15af34c3f9a50ab332dcab5c5d1a&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean commit SHA:&lt;/strong&gt; &lt;code&gt;d8e511bb7e46c8fa91c7c3e4e85a9db15a41f89c&lt;/code&gt; (tag restored March 29)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposure window:&lt;/strong&gt; March 27 - March 29&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Four projects. Eight days. Two of them were security scanners. The tools organizations deploy to &lt;em&gt;find&lt;/em&gt; backdoors were &lt;em&gt;carrying&lt;/em&gt; backdoors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not a footnote in a threat report. That's a fundamental trust failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Mutable Tags Became the Weapon
&lt;/h2&gt;

&lt;p&gt;The PyPI compromises on LiteLLM and Telnyx followed a pattern supply chain researchers have documented dozens of times: steal maintainer credentials, push a poisoned package version, wait for &lt;code&gt;pip install&lt;/code&gt; to do the rest. Familiar. Still effective.&lt;/p&gt;

&lt;p&gt;The GitHub Actions attacks were nastier.&lt;/p&gt;

&lt;p&gt;Most workflows reference actions by a mutable tag. Here's what a standard Trivy setup looked like in thousands of repositories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Trivy vulnerability scanner&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-app:latest'&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CRITICAL,HIGH'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;@v1&lt;/code&gt; is a Git tag. It points to whatever commit the maintainer last associated with it. Tags in Git aren't fixed. Anyone with write access can delete a tag and recreate it pointing at a completely different commit. There's no audit log visible to consumers. No notification fires. No version number changes.&lt;/p&gt;

&lt;p&gt;TeamPCP gained write access to the action repositories (compromised maintainer tokens remain the leading theory, though full initial access details haven't been disclosed) and performed this exact operation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cloned the legitimate action code&lt;/li&gt;
&lt;li&gt;Added SANDCLOCK as a secondary payload that ran silently after the scanner completed&lt;/li&gt;
&lt;li&gt;Moved the &lt;code&gt;@v1&lt;/code&gt; tag to point at the new, poisoned commit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The action still ran the scanner. Still produced the expected output. Still reported vulnerabilities in other people's code. It just &lt;em&gt;also&lt;/em&gt; quietly harvested every credential in the CI/CD environment and shipped them to attacker-controlled infrastructure over HTTPS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What developers thought they were running:&lt;/span&gt;
&lt;span class="c1"&gt;#   trivy scan → report vulnerabilities → done&lt;/span&gt;

&lt;span class="c1"&gt;# What actually ran:&lt;/span&gt;
&lt;span class="c1"&gt;#   trivy scan → report vulnerabilities → harvest SSH keys,&lt;/span&gt;
&lt;span class="c1"&gt;#   AWS creds, GitHub tokens → exfiltrate to attacker C2 → done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody noticed for days. The scanner scanned. The malware ran. The output looked normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SANDCLOCK Harvests
&lt;/h2&gt;

&lt;p&gt;SANDCLOCK isn't ransomware. It isn't a cryptominer. It's a credential vacuum designed for stealth over spectacle.&lt;/p&gt;

&lt;p&gt;Once running inside a CI/CD environment, it targets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH private keys.&lt;/strong&gt; It searches &lt;code&gt;~/.ssh/&lt;/code&gt; for &lt;code&gt;id_rsa&lt;/code&gt;, &lt;code&gt;id_ed25519&lt;/code&gt;, and anything else that looks like a private key. CI/CD runners frequently hold keys with push access to production repositories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every environment variable.&lt;/strong&gt; &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;, &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, &lt;code&gt;NPM_TOKEN&lt;/code&gt;, database connection strings, third-party API keys. Pipelines need credentials to deploy, which makes their runtime environments a buffet of secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud provider credential files.&lt;/strong&gt; Beyond environment variables, SANDCLOCK specifically scrapes &lt;code&gt;~/.aws/credentials&lt;/code&gt;, &lt;code&gt;~/.config/gcloud/application_default_credentials.json&lt;/code&gt;, and &lt;code&gt;~/.azure/&lt;/code&gt;. Persistent credential files on build runners are gold for lateral movement into cloud infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git configuration and stored credentials.&lt;/strong&gt; The &lt;code&gt;.gitconfig&lt;/code&gt; file and any cached Git credentials, enabling the attacker to push code to repositories the compromised runner can reach. This creates potential for the attack to spread itself.&lt;/p&gt;

&lt;p&gt;Exfiltration happened over HTTPS to domains mimicking legitimate analytics and telemetry endpoints. Blending with normal outbound CI/CD traffic made network detection brutally hard.&lt;/p&gt;

&lt;p&gt;The entire design philosophy: grab everything, stay quiet, enable larger attacks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Whether You Got Hit
&lt;/h2&gt;

&lt;p&gt;This section matters more than anything else in this post.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trivy
&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;# Find all workflow files referencing Trivy&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"aquasecurity/trivy-action"&lt;/span&gt; .github/workflows/

&lt;span class="c"&gt;# Check your GitHub Actions run logs between March 22-25&lt;/span&gt;
&lt;span class="c"&gt;# Look at the "Set up job" step to see which commit SHA was resolved&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the resolved SHA against the compromised commit &lt;code&gt;b4b587a89b42c8b9b4494c2e3f58f5e33eb937bb&lt;/code&gt;. If it matches, that run was poisoned.&lt;/p&gt;

&lt;h3&gt;
  
  
  KICS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"checkmarx/kics"&lt;/span&gt; .github/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare resolved SHAs from runs between March 27-29 against &lt;code&gt;94a2d2cfee7c15af34c3f9a50ab332dcab5c5d1a&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  LiteLLM
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip show litellm
pip &lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;pip show &lt;span class="nt"&gt;-f&lt;/span&gt; litellm | &lt;span class="nb"&gt;grep &lt;/span&gt;Location | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/litellm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the installed version falls between &lt;code&gt;1.56.3&lt;/code&gt; and &lt;code&gt;1.56.5&lt;/code&gt;, reinstall from the verified clean release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;litellm&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.56.6 &lt;span class="nt"&gt;--force-reinstall&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Telnyx
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip show telnyx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Versions &lt;code&gt;2.1.0&lt;/code&gt; through &lt;code&gt;2.1.2&lt;/code&gt; are compromised. Clean target: &lt;code&gt;2.1.3&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  General SANDCLOCK Indicators
&lt;/h3&gt;

&lt;p&gt;Look for these across any potentially affected system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unexpected outbound HTTPS connections from CI/CD runners to unfamiliar domains during build steps&lt;/li&gt;
&lt;li&gt;Processes reading SSH keys or cloud credential files that have no business touching them&lt;/li&gt;
&lt;li&gt;Modified or newly created files in &lt;code&gt;/tmp&lt;/code&gt; or runner workspace directories that don't belong to the build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If credentials were exposed, rotate everything.&lt;/strong&gt; Not just the ones that seem important. SANDCLOCK grabs anything it can reach, and guessing which secrets the attacker copied is a losing game.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Broader March 2026 Context
&lt;/h2&gt;

&lt;p&gt;This wasn't happening in a vacuum. March 2026 was a rough month for supply chain security across the board. North Korea-linked group UNC1069 was running a separate campaign targeting the &lt;code&gt;axios&lt;/code&gt; npm package. Zscaler's threat research team documented a measurable surge in supply chain attacks across multiple package ecosystems.&lt;/p&gt;

&lt;p&gt;But TeamPCP's campaign stands out because of target selection. Hitting LiteLLM and Telnyx is standard supply chain opportunism: popular packages, lots of installs, harvest at scale. Hitting Trivy and KICS shows calculation.&lt;/p&gt;

&lt;p&gt;Security tools run with elevated permissions. They access source code, container images, infrastructure configurations, and the CI/CD secrets required for deployment. Organizations explicitly trust them. When a security team maps their pipeline's attack surface, the vulnerability scanner is the last thing they'd suspect.&lt;/p&gt;

&lt;p&gt;Which is exactly what makes it the best target.&lt;/p&gt;

&lt;p&gt;There's a compounding irony here that's hard to overstate. The organizations diligent enough to integrate Trivy or KICS into their pipelines were the ones who got hit. Teams running zero security scanning? Completely unaffected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defensive Measures That Would Have Caught This
&lt;/h2&gt;

&lt;p&gt;No silver bullet exists. But specific steps would have prevented or contained this particular campaign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin GitHub Actions to full commit SHAs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the single highest-impact change. A mutable tag is a redirect anyone with write access can silently change. A commit SHA is fixed forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Vulnerable (mutable tag):&lt;/span&gt;
&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@v1&lt;/span&gt;

&lt;span class="c1"&gt;# Hardened (immutable SHA):&lt;/span&gt;
&lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@f1ef53dab1f0a26b0f9cda0f94e66e3f93ae6375&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools like &lt;a href="https://github.com/step-security/harden-runner" rel="noopener noreferrer"&gt;StepSecurity Harden-Runner&lt;/a&gt; and &lt;a href="https://github.com/mheap/pin-github-action" rel="noopener noreferrer"&gt;pin-github-action&lt;/a&gt; can enforce SHA pinning across all workflows automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable pip hash verification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;--require-hashes&lt;/code&gt; and maintain hash-pinned requirements files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# requirements.txt with hash pinning
litellm==1.56.6 \
    --hash=sha256:a1b2c3d4e5f6...verified_hash_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A modified package with a different hash won't install. Period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Restrict CI/CD runner egress traffic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most build steps don't need unrestricted outbound internet access. Allowlisting known-good domains and alerting on anything else would have flagged SANDCLOCK's exfiltration within minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destroy runners after every job.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ephemeral runners prevent credential files from accumulating. Combine this with just-in-time secret provisioning that injects only what each step needs for only the duration it needs them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimize workflow permissions.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="c1"&gt;# Don't grant write access unless the step genuinely needs it&lt;/span&gt;
  &lt;span class="c1"&gt;# Don't expose deployment secrets to scanning steps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Trivy scan doesn't need write access to the repository. It doesn't need deployment secrets. Least privilege limits blast radius.&lt;/p&gt;

&lt;p&gt;
  Checklist: Post-Incident Response
  &lt;p&gt;If any of these four projects ran in your environment during the affected windows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify every CI/CD run that used the compromised versions&lt;/li&gt;
&lt;li&gt;List every secret, token, and credential accessible to those runs&lt;/li&gt;
&lt;li&gt;Rotate all of them. Not some. All&lt;/li&gt;
&lt;li&gt;Audit Git history for unexpected commits pushed with stolen credentials&lt;/li&gt;
&lt;li&gt;Check cloud provider audit logs for unauthorized API calls&lt;/li&gt;
&lt;li&gt;Pin all GitHub Actions to commit SHAs going forward&lt;/li&gt;
&lt;li&gt;Add hash verification to all pip install commands&lt;/li&gt;
&lt;li&gt;Restrict runner network egress to allowlisted domains&lt;/li&gt;
&lt;li&gt;Subscribe to security advisories for every action and package in your pipeline&lt;/li&gt;
&lt;/ol&gt;



&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trust Problem Doesn't Have a Clean Fix
&lt;/h2&gt;

&lt;p&gt;Every dependency is a trust decision. Every GitHub Action, every PyPI package, every base image. Developers make hundreds of these calls per project, mostly without thinking about it.&lt;/p&gt;

&lt;p&gt;The standard advice says: verify everything, audit dependencies, read source code, check hashes. In practice, nobody does this for every package in every project on every update. The volume is too high and the tooling to make it manageable is still catching up.&lt;/p&gt;

&lt;p&gt;What March 2026 demonstrated wasn't a new category of attack. Supply chain compromises have been documented for years. What it demonstrated is that the trust boundary extends further than most teams realize. The vulnerability scanner isn't outside the attack surface. It's part of it. The IaC checker isn't a neutral observer. It's running code on your infrastructure with access to your secrets, just like everything else in the pipeline.&lt;/p&gt;

&lt;p&gt;Treating security tools as inherently trustworthy &lt;em&gt;because&lt;/em&gt; they're security tools is the same mistake as treating any dependency as safe because it's popular. Popularity doesn't prevent compromise. It incentivizes it.&lt;/p&gt;

&lt;p&gt;The teams that weathered this best weren't the ones with the most security tools. They were the ones who treated their security tools with the same suspicion they'd apply to any third-party code. Pinned versions. Restricted permissions. Monitored behavior. Rotated credentials fast when the advisories dropped.&lt;/p&gt;

&lt;p&gt;Four projects. Eight days. Two scanners turned into attack vectors. Thousands of pipelines silently compromised.&lt;/p&gt;

&lt;p&gt;The question isn't whether this happens again. It's whether the pipeline will catch it when it does.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>Coinbase Fired Engineers Who Didn't Learn AI in Five Days. The Industry Barely Flinched.</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 05 Apr 2026 17:21:35 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/coinbase-fired-engineers-who-didnt-learn-ai-in-five-days-the-industry-barely-flinched-34j7</link>
      <guid>https://dev.to/gabrielanhaia/coinbase-fired-engineers-who-didnt-learn-ai-in-five-days-the-industry-barely-flinched-34j7</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;gabrielanhaia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Monday morning. A message goes out to every engineer at Coinbase: learn GitHub Copilot and Cursor. Not next quarter. Not when the current sprint wraps. By Friday.&lt;/p&gt;

&lt;p&gt;Saturday, the holdouts sit across from Brian Armstrong himself. The ones who can't produce a "good reason" for skipping the tools don't make it to the following week.&lt;/p&gt;

&lt;p&gt;That's not a thought experiment. It happened at one of the largest crypto exchanges on the planet, and Armstrong described it on John Collison's &lt;em&gt;Cheeky Pint&lt;/em&gt; podcast with the relaxed confidence of someone who's already decided the conversation is over.&lt;/p&gt;

&lt;p&gt;The tech industry's response? A collective shrug.&lt;/p&gt;

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

&lt;p&gt;Five business days. That's the window Armstrong gave his engineering organization to become proficient with AI-assisted coding tools. Not "try them out." Not "evaluate whether they fit your workflow." Proficient.&lt;/p&gt;

&lt;p&gt;Here's the sequence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monday:&lt;/strong&gt; Mandate goes out. Every engineer must adopt Copilot and Cursor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tuesday through Friday:&lt;/strong&gt; Engineers are expected to integrate these tools into daily work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saturday:&lt;/strong&gt; Engineers who haven't complied sit down with the CEO personally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After the meeting:&lt;/strong&gt; Those without an acceptable justification are terminated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Armstrong didn't frame this as optional. Monthly team meetings now track AI usage metrics. Direct reporting on adoption rates flows upward. The initial five-day sprint was just the shock; the ongoing surveillance is the system.&lt;/p&gt;

&lt;p&gt;Coinbase currently reports that 33% of its new code comes from AI assistance. Armstrong's stated target: 50%.&lt;/p&gt;

&lt;p&gt;Those numbers sound bold. They also collapse under the slightest scrutiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does "33% AI Code" Actually Measure?
&lt;/h2&gt;

&lt;p&gt;Nobody in the industry has settled on a definition. Does "AI-generated" mean Copilot wrote the first draft and a human approved it without changes? Does it mean a developer wrote most of a function and Copilot filled in the boilerplate? When an engineer accepts an AI suggestion, then refactors it until the original is unrecognizable, whose code is that?&lt;/p&gt;

&lt;p&gt;Coinbase hasn't published its methodology. The 33% figure most likely tracks acceptance rates in Copilot and Cursor. How often an engineer hits "tab" instead of typing manually.&lt;/p&gt;

&lt;p&gt;That tells you something about adoption. It tells you almost nothing about quality.&lt;/p&gt;

&lt;p&gt;Consider what gets rewarded when the metric is volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Team A: Tight, well-abstracted code&lt;/span&gt;
&lt;span class="c1"&gt;// 200 lines, 10% AI-suggested&lt;/span&gt;
&lt;span class="c1"&gt;// Zero production incidents this quarter&lt;/span&gt;

&lt;span class="c1"&gt;// Team B: Verbose, repetitive implementation&lt;/span&gt;
&lt;span class="c1"&gt;// 800 lines, 55% AI-suggested&lt;/span&gt;
&lt;span class="c1"&gt;// Same feature, three incidents this quarter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Team B looks better on the dashboard. Team A looks better in production. When Armstrong says the goal is 50%, the uncomfortable follow-up is: 50% of &lt;em&gt;what&lt;/em&gt;, measured &lt;em&gt;how&lt;/em&gt;, optimized for &lt;em&gt;whom&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;Lines of code? Files touched? Pull requests with AI involvement? Each of those captures something different. Optimizing for the wrong one creates incentives that actively degrade software quality.&lt;/p&gt;

&lt;p&gt;Goodhart's Law hasn't taken a day off. When a measure becomes a target, it stops being a good measure. A team that inflates its AI percentage by accepting mediocre suggestions looks phenomenal in the monthly review and catastrophic in the incident postmortem.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Adoption Mandates: Who's Doing What
&lt;/h2&gt;

&lt;p&gt;Coinbase isn't alone in pushing AI tools. But the enforcement spectrum across major companies is wide.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;AI Stance&lt;/th&gt;
&lt;th&gt;Enforcement&lt;/th&gt;
&lt;th&gt;Timeline&lt;/th&gt;
&lt;th&gt;Consequence for Non-Adoption&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coinbase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mandatory proficiency in Copilot/Cursor&lt;/td&gt;
&lt;td&gt;CEO-level meetings, monthly metrics tracking&lt;/td&gt;
&lt;td&gt;5 business days&lt;/td&gt;
&lt;td&gt;Termination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shopify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Baseline expectation" per CEO memo&lt;/td&gt;
&lt;td&gt;Teams must prove AI can't do a task before requesting headcount&lt;/td&gt;
&lt;td&gt;Gradual, no fixed deadline&lt;/td&gt;
&lt;td&gt;Hiring/resource implications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Internal AI assistants encouraged&lt;/td&gt;
&lt;td&gt;Usage tracked, discussed in team settings&lt;/td&gt;
&lt;td&gt;Ongoing, multi-year rollout&lt;/td&gt;
&lt;td&gt;No reported direct consequences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CodeWhisperer pushed through AWS ecosystem&lt;/td&gt;
&lt;td&gt;Built into product workflows; free for developers&lt;/td&gt;
&lt;td&gt;Organic adoption&lt;/td&gt;
&lt;td&gt;None reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stripe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heavy internal AI usage&lt;/td&gt;
&lt;td&gt;Tool choices bubble up from teams&lt;/td&gt;
&lt;td&gt;Team-driven pace&lt;/td&gt;
&lt;td&gt;None reported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Meta&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI tools integrated into internal dev environment&lt;/td&gt;
&lt;td&gt;Engineering leadership tracks adoption&lt;/td&gt;
&lt;td&gt;Rolling&lt;/td&gt;
&lt;td&gt;None reported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is visible. Most large engineering organizations treat AI adoption as a product problem or a culture problem. Build good tools, make them accessible, let teams experiment, track what sticks.&lt;/p&gt;

&lt;p&gt;Coinbase treats it as a compliance problem. Use this tool. Use it now. Prove you're using it. Or leave.&lt;/p&gt;

&lt;p&gt;What separates Coinbase from the pack isn't the pro-AI position. Virtually every major tech company shares that position. It's the velocity of the mandate and the severity of the consequences. Shopify's Tobi Lutke wrote a memo reshaping how the company thinks about staffing around AI capabilities. That's aggressive. But nobody at Shopify reportedly lost their job for not learning a text editor plugin in a business week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Decides What Counts as a "Good Reason"?
&lt;/h2&gt;

&lt;p&gt;Armstrong used that phrase. Engineers who missed the Friday deadline needed a "good reason." Two words carrying the weight of people's livelihoods.&lt;/p&gt;

&lt;p&gt;Picture the room. A software engineer sits across from the founder-CEO of a publicly traded company valued in the tens of billions. The engineer must justify why they didn't learn a particular code completion tool in five business days. The CEO gets to decide whether the justification holds up.&lt;/p&gt;

&lt;p&gt;What qualifies?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The tool doesn't work well with our legacy codebase."&lt;br&gt;
Is that a good reason, or an excuse?&lt;/p&gt;

&lt;p&gt;"I was shipping a critical feature all week and didn't have time."&lt;br&gt;
Does that fly, or should the engineer have found time anyway?&lt;/p&gt;

&lt;p&gt;"I have concerns about code quality and IP ownership."&lt;br&gt;
Is that principled skepticism, or resistance to progress?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody outside that Saturday room knows the answers. That's the whole point. When the standard is subjective and the judge holds absolute power over employment, "good reason" means whatever the CEO decides it means at that moment on that particular Saturday morning.&lt;/p&gt;

&lt;p&gt;
  What "psychological safety" looks like after this kind of mandate
  &lt;p&gt;Engineers who survived the Saturday meeting now work in a company where the CEO has shown willingness to fire people over tool preferences on a one-week timeline. They'll use the tools. Of course they will.&lt;/p&gt;

&lt;p&gt;But compliance and buy-in are different animals that wear the same mask. Compliance means engineers accept AI suggestions to hit metrics. Buy-in means engineers use AI tools because they genuinely believe those tools make their work better.&lt;/p&gt;

&lt;p&gt;Compliance looks identical to buy-in in every dashboard, every monthly report, every podcast talking point. It reveals itself only when something goes wrong. When a production outage traces back to an AI suggestion nobody questioned because questioning felt like career risk.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;This dynamic isn't unique to AI tools. It surfaces whenever top-down mandates replace collaborative decision-making. The mandate might even be correct. AI coding tools probably &lt;em&gt;should&lt;/em&gt; be part of every working engineer's setup. But "you should learn this" and "learn this in five days or you're fired" land differently in terms of trust, autonomy, and the kind of engineering culture that produces reliable software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Speed Fallacy
&lt;/h2&gt;

&lt;p&gt;There's an assumption baked into the 33%-to-50% target that deserves direct challenge: that more AI-generated code is inherently better.&lt;/p&gt;

&lt;p&gt;Faster code production matters when the bottleneck is typing speed. In most engineering organizations, typing speed hasn't been the constraint since the invention of copy-paste. The hard problems live elsewhere. Understanding requirements that contradict each other. Designing systems that handle edge cases gracefully. Debugging production failures at 2 AM when the on-call page hits. Making architectural decisions that won't haunt the team eighteen months from now.&lt;/p&gt;

&lt;p&gt;AI tools genuinely help with certain tasks. Generating boilerplate. Writing tests from existing implementations. Filling in repetitive patterns. Suggesting implementations of well-understood algorithms. No serious engineer disputes those gains.&lt;/p&gt;

&lt;p&gt;But volume without direction is just a faster way to accumulate technical debt.&lt;/p&gt;

&lt;p&gt;Google's internal research on AI-assisted coding found that while these tools increased code production speed, they didn't meaningfully reduce time spent on code review, debugging, or incident response. The total elapsed time from "start coding" to "feature works in production" improved modestly. Real gains, but far smaller than raw generation metrics would suggest.&lt;/p&gt;

&lt;p&gt;Coinbase's 33% figure tells a story about adoption velocity. It tells almost nothing about whether the engineering team ships better software, resolves incidents faster, or produces fewer bugs per feature. Those metrics presumably exist inside the company. Armstrong didn't choose to share them on the podcast. That silence says something.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Working Engineers Should Take From This
&lt;/h2&gt;

&lt;p&gt;Underneath the power dynamics analysis sits a practical truth: AI coding tools are becoming a baseline expectation across the entire industry. Not every company will fire engineers over it. Most won't. The direction, though, is unmistakable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learn the tools before someone tells you to.&lt;/strong&gt; Spend a week with Copilot or Cursor on a side project. Discover what they handle well (boilerplate, test scaffolding, code explanation) and where they fall apart (security-sensitive logic, novel architecture, anything requiring deep domain context). Informed opinions beat no experience every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure your own output honestly.&lt;/strong&gt; If AI tools make you faster at producing correct, maintainable code, that's a win. If they make you faster at producing &lt;em&gt;code&lt;/em&gt; while your bug rate climbs and your review cycles lengthen, that's a different story. Track whether the suggestions you accept tend to survive untouched or get torn apart in review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate tool competence from tool dependence.&lt;/strong&gt; Being able to use Copilot is a skill. Being unable to function without it is a liability. The engineers who get the most from AI tools are the ones who understand the underlying code well enough to evaluate whether a suggestion makes sense. That requires the same fundamentals it always has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch what your company measures.&lt;/strong&gt; If leadership starts tracking AI adoption percentages, think hard about what behavior that incentivizes. A team optimizing for the metric will make different choices than a team optimizing for software quality. Those paths diverge further than most managers realize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Precedent That Matters More Than the Tools
&lt;/h2&gt;

&lt;p&gt;Armstrong's mandate will work by his own metrics. AI adoption at Coinbase will reach 50%. The monthly meetings will produce numbers that look great in a board presentation. Engineers will use the tools because the alternative is unemployment.&lt;/p&gt;

&lt;p&gt;But strip away the AI angle and the structure is bare. A CEO decides a specific technology is mandatory. Engineers get five days. Holdouts get fired. Compliance is tracked monthly. The "good reason" standard is defined by one person with unchecked authority over the outcome.&lt;/p&gt;

&lt;p&gt;Swap out "AI coding tools" for any technology. The architecture of the mandate is identical.&lt;/p&gt;

&lt;p&gt;The question that should make engineers uncomfortable isn't "should I learn AI tools?" Obviously yes. The question is: who gets to decide how fast developers must change their workflows, with what consequences, and based on whose definition of success?&lt;/p&gt;

&lt;p&gt;At most companies, tool adoption happens through grassroots enthusiasm, team experimentation, and gradual standardization. Slower. Messier. Won't generate a viral podcast clip. But it tends to produce genuine adoption rather than performative compliance, and it doesn't require anyone to justify their professional judgment to the CEO on a Saturday.&lt;/p&gt;

&lt;p&gt;Armstrong chose speed over consensus. That's his prerogative as a founder-CEO. It might even be the right strategic call for Coinbase specifically, given how fast AI capabilities are evolving and how volatile the crypto market remains.&lt;/p&gt;

&lt;p&gt;Every engineer watching from outside should notice what happened, though. Not the AI part. The part where five days was deemed sufficient to reshape how an entire engineering organization works, and where the penalty for falling short was a meeting that ended careers.&lt;/p&gt;

&lt;p&gt;Tools will come and go. Copilot might dominate for years or get replaced next quarter. The precedent of mandate-and-fire on a five-day timeline? That sticks around much longer than any code completion engine.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>They Forced a Junior to Use AI. Then Fired Him for the Bugs It Wrote.</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 05 Apr 2026 17:20:49 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/they-forced-a-junior-to-use-ai-then-fired-him-for-the-bugs-it-wrote-407k</link>
      <guid>https://dev.to/gabrielanhaia/they-forced-a-junior-to-use-ai-then-fired-him-for-the-bugs-it-wrote-407k</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;gabrielanhaia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Two Bugs and a Cardboard Box
&lt;/h2&gt;

&lt;p&gt;A junior engineer at an AI startup shipped two bugs to production. Got fired for it.&lt;/p&gt;

&lt;p&gt;Read that again. Two bugs. From a first-year developer. At a company that told him to use AI coding tools. That's not a performance issue. That's a Tuesday.&lt;/p&gt;

&lt;p&gt;The company had "strongly encouraged" every engineer to lean on tools like Cursor. Ship faster. Write more. Let the machine handle the boring parts. So this junior did exactly what he was told. The machine produced code. The code had bugs. The bugs hit production. And the person who followed instructions got walked out of the building.&lt;/p&gt;

&lt;p&gt;Reddit caught fire. Thousands of comments ripped into the company for building a trap and punishing the person who fell into it. Because that's what happened. Leadership set the rules, picked the tools, decided speed mattered more than correctness, and then acted shocked when correctness suffered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Coinbase Connection
&lt;/h2&gt;

&lt;p&gt;This didn't happen in a vacuum. It happened in the same industry where Coinbase CEO Brian Armstrong publicly ordered every engineer to adopt AI tools and gave them &lt;strong&gt;one week&lt;/strong&gt; to get proficient. Seven days. Not a quarter. Not even a full sprint cycle. Seven calendar days to restructure how they write software.&lt;/p&gt;

&lt;p&gt;Armstrong's stated goal: half of Coinbase's codebase generated by AI. That figure currently sits at 33% and climbing. For a company that handles billions in cryptocurrency transactions, where a single authorization bug in a trading endpoint could drain customer wallets.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One week to learn tools that produce 1.7x more bugs than human-written code, according to CodeRabbit's research. What could go wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The pattern writes itself on a whiteboard. Company mandates AI tools. AI tools produce code at volume with known quality gaps. Bugs ship. Someone gets blamed. That someone is never the executive who signed the mandate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "AI-First Development" Looks Like from the Bottom
&lt;/h2&gt;

&lt;p&gt;Picture the junior's position for thirty seconds.&lt;/p&gt;

&lt;p&gt;Six months into a first real engineering job. Leadership sends an all-hands email about "AI-first development." The team lead starts tracking velocity in PRs per week. Cursor gets installed on every machine. There's a Slack channel called #ai-wins where people post screenshots of generated code. Nobody creates a channel called #ai-failures. That channel would be too active.&lt;/p&gt;

&lt;p&gt;A 23-year-old doesn't have the scar tissue to know what AI gets wrong. They haven't debugged a race condition at 2 AM. Haven't watched a silent error swallow a payment. Haven't spent three days tracking down a missing authorization check buried inside generated code that &lt;em&gt;looked&lt;/em&gt; flawless.&lt;/p&gt;

&lt;p&gt;So the junior does what he's told. Uses the tool. Ships the output. And when it breaks, the company fires the developer, not the process.&lt;/p&gt;

&lt;p&gt;That's not accountability. That's a sacrifice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug That Passes Every Test
&lt;/h2&gt;

&lt;p&gt;Here's what people outside engineering don't grasp about AI-generated bugs: they don't look broken. They look clean, well-structured, idiomatic. The code does the right thing in every scenario except the one that costs money.&lt;/p&gt;

&lt;p&gt;Consider a discount calculation service for an e-commerce platform. The AI generates this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AI-generated: applies promotional discount to cart total
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart_total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discount_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cart_total&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;percentage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cart_total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cart_total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cart_total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Readable. Handles percentage and fixed discounts. Returns the original total for invalid codes. Tests pass because the test suite covers valid discounts, invalid codes, and both discount types.&lt;/p&gt;

&lt;p&gt;Ships to production. Works fine for two weeks. Nobody panics.&lt;/p&gt;

&lt;p&gt;Then a customer applies a $50 fixed discount to a $30 cart. The function returns &lt;strong&gt;negative twenty dollars&lt;/strong&gt;. The payment processor reads that as a $20 &lt;em&gt;credit&lt;/em&gt; to the customer's account. The company bleeds money on every order where the discount exceeds the cart total. Nobody catches it for eleven days because the monitoring dashboard tracks transaction volume, not transaction polarity.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart_total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discount_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cart_total&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;percentage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;discounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cart_total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;discounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cart_total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cart_total&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;lt;-- the entire fix
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. &lt;code&gt;max(discounted, 0.0)&lt;/code&gt;. That's the gap between working software and an eleven-day revenue leak. A senior who's been burned by negative totals catches this in review without breaking stride. The AI didn't flag it because negative totals weren't in its training data. The junior didn't flag it because nobody taught him to look.&lt;/p&gt;

&lt;p&gt;And this is who gets fired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and After: What Proper Review Looks Like
&lt;/h2&gt;

&lt;p&gt;The uncomfortable part: that bug shouldn't have reached production regardless of who wrote it. Not the junior. Not the AI. Not both together. The company's review process failed, assuming one existed at all.&lt;/p&gt;

&lt;p&gt;Here's the difference between a team that ships AI-generated code safely and one that ships termination letters.&lt;/p&gt;

&lt;p&gt;
  The broken process (what likely happened)
  &lt;ol&gt;
&lt;li&gt;Junior generates code with AI tool&lt;/li&gt;
&lt;li&gt;Junior opens PR&lt;/li&gt;
&lt;li&gt;Another junior or mid-level dev skims it, sees clean code, approves&lt;/li&gt;
&lt;li&gt;CI runs tests. Tests pass (because nobody wrote edge-case tests)&lt;/li&gt;
&lt;li&gt;Code merges and deploys&lt;/li&gt;
&lt;li&gt;Bug hits production&lt;/li&gt;
&lt;li&gt;Junior gets blamed&lt;/li&gt;
&lt;/ol&gt;



&lt;/p&gt;

&lt;p&gt;
  The process that actually works
  &lt;p&gt;&lt;strong&gt;PR Creation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every PR containing AI-assisted code gets an automatic &lt;code&gt;ai-generated&lt;/code&gt; label&lt;/li&gt;
&lt;li&gt;No exceptions. The team needs to know what they're reviewing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Review Assignment:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-labeled PRs route to a senior engineer. Not another junior. Someone who's spent years building instinct for boundary conditions, missing auth checks, and silent failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Edge-Case Testing Required Before Merge:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_discount_does_not_go_negative&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fixed discount larger than cart total should return 0.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAVE50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_percentage_discount_at_boundary&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;100% discount should zero the cart, not go below.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FULL100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_zero_cart_with_discount&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Discount applied to empty cart should return 0.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAVE10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_negative_cart_rejected&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Negative cart totals should raise, not silently compute.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAVE10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pre-Merge Checklist for AI-Assisted Code:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this handle null, empty, and zero inputs?&lt;/li&gt;
&lt;li&gt;What happens at boundaries? Negative numbers, max values, empty collections?&lt;/li&gt;
&lt;li&gt;Are there race conditions under concurrent requests?&lt;/li&gt;
&lt;li&gt;Does this check authorization, not just authentication?&lt;/li&gt;
&lt;li&gt;What fails silently? Where do errors get swallowed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Post-Incident:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postmortem examines the process, not just the person. Was the code reviewed? By whom? Were edge-case tests written? If the answer to any of those is "no," the process failed. Not the developer.&lt;/li&gt;
&lt;/ul&gt;



&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;That second process isn't revolutionary. It's barely novel. But the company that fired this junior didn't have it. Neither do most companies racing to hit their AI adoption KPIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers Nobody Puts on a Slide
&lt;/h2&gt;

&lt;p&gt;CodeRabbit's research confirms what this story shows in miniature: AI-generated code produces &lt;strong&gt;1.7x more bugs&lt;/strong&gt; than human-written code. Not theoretical risks. Production-breaking, customer-facing defects at nearly twice the rate.&lt;/p&gt;

&lt;p&gt;Incidents per pull request climbed 23.5% across teams that adopted AI tools aggressively. Mean time to resolve those incidents went &lt;em&gt;up&lt;/em&gt; by 62%. Why? Because the people who understood the systems well enough to fix things fast were often the same people whose roles got "optimized" away.&lt;/p&gt;

&lt;p&gt;Combine those numbers with forced adoption. A company pushes developers toward tools that produce 1.7x more bugs, measures success by output volume, underinvests in review, and then fires the lowest-ranking person when the math does what math does.&lt;/p&gt;

&lt;p&gt;That's not a management strategy. That's a liability time bomb with a junior developer strapped to the front.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accountability Flows Downhill. It Shouldn't.
&lt;/h2&gt;

&lt;p&gt;There's an infuriating pattern in how organizations handle tool failures.&lt;/p&gt;

&lt;p&gt;When enterprise software causes problems, the vendor gets blamed. When a new database corrupts data, the architecture team faces scrutiny. When a CI/CD pipeline introduces regressions, the platform team holds a retro and writes an action plan.&lt;/p&gt;

&lt;p&gt;But when mandated AI coding tools produce bugs? The individual developer gets fired. The tool keeps its seat license. The mandate stays in place. Leadership writes a blog post about "AI-first culture."&lt;/p&gt;

&lt;p&gt;This only works in one direction: downward. Leadership mandates the tool. Leadership sets velocity targets. Leadership decides PRs-per-week is the metric that matters. Leadership skips investing in review infrastructure. And when the entirely predictable outcome arrives, leadership fires the person with the least power to have changed any of those decisions.&lt;/p&gt;

&lt;p&gt;That junior didn't choose the tool. Didn't set the targets. Didn't design the review process. Didn't decide that speed mattered more than correctness. He just happened to be holding the code when the music stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Armstrong's 50% and What It Means for Coinbase
&lt;/h2&gt;

&lt;p&gt;Think about what 50% AI-generated code means for a financial services company.&lt;/p&gt;

&lt;p&gt;Coinbase isn't a social media app where the worst bug shows someone the wrong meme. Regulatory compliance isn't optional. Security flaws don't just cost reputation; they cost real money from real customer accounts. A race condition in the order matching engine could create phantom trades. A missing boundary check in a withdrawal endpoint could let attackers drain funds.&lt;/p&gt;

&lt;p&gt;Fifty percent AI-generated code in that environment, without proportional investment in verification infrastructure, is a bet that the tools won't produce the exact class of bugs they're statistically proven to produce. And the engineers expected to catch those bugs before they ship? They had seven days to learn the tools that wrote them.&lt;/p&gt;

&lt;p&gt;The junior who got fired at that AI startup is a preview. Scale the same dynamic to a company managing customer funds, and the stakes stop being about one person's career.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Reddit Got Right
&lt;/h2&gt;

&lt;p&gt;Reddit nailed the core point: firing a junior over two bugs is absurd. Senior engineers ship bugs. Staff engineers ship bugs. Distinguished engineers ship bugs. The difference is that experienced engineers typically work inside processes with enough review layers that their bugs get caught before customers see them.&lt;/p&gt;

&lt;p&gt;Reddit also correctly identified the systemic failure. Mandating tools without building guardrails. Measuring velocity without measuring quality. Skipping the investment that makes AI-assisted code safe to ship.&lt;/p&gt;

&lt;p&gt;Where the conversation went sideways: treating this as an isolated case of bad management. It's not isolated. It's structural. The incentive system across the industry currently rewards AI adoption speed and punishes caution. Companies that slow down to build review infrastructure don't get praised for responsibility. They get asked by their board why adoption metrics lag behind competitors who fired their way to faster numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Actually Happen
&lt;/h2&gt;

&lt;p&gt;Stop firing juniors for predictable outcomes. That's the floor. Everything else builds on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fund the verification layer before mandating the tools.&lt;/strong&gt; If leadership wants 50% AI-generated code, leadership needs to pay for the review infrastructure that makes that safe. Senior reviewers. AI-specific checklists. Boundary-focused test requirements. Automated analysis that flags common AI failure patterns. This costs money. Far less money than production incidents and wrongful termination suits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put defect rates on the same dashboard as velocity.&lt;/strong&gt; Any team metric that shows PRs-per-sprint without showing incidents-per-PR is a lie of omission. Both numbers go on the same slide, or neither does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build structured onboarding for AI-assisted development.&lt;/strong&gt; Juniors need to learn what AI gets wrong before they're asked to use it in production. Curated examples of AI failures. Paired review sessions with senior engineers. A ramp-up period that's longer than one calendar week. Build the judgment first, deploy the tools second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make accountability proportional to authority.&lt;/strong&gt; The person who mandates the tool carries more responsibility for the tool's failures than the person told to use it. When AI-generated code breaks production, the postmortem examines the decision chain, not just the last person who clicked "approve."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat AI output like untrusted input.&lt;/strong&gt; Every AI-generated code block deserves the same skepticism as user input in a web form. Validate it. Test its boundaries. Assume it's wrong until proven otherwise. Teams that internalize this will ship fewer bugs than teams that treat AI output like a trusted colleague's work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protect juniors during this transition.&lt;/strong&gt; Junior developers are the most vulnerable to AI-generated bugs because they have the least experience spotting them. That's not a reason to fire juniors. It's a reason to invest harder in mentoring them. The senior engineers of 2031 are the juniors getting fired in 2026 for bugs they didn't write and couldn't have caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question Nobody in Leadership Wants to Sit With
&lt;/h2&gt;

&lt;p&gt;If a company mandates a tool, and that tool produces bad output, and an employee ships that output in good faith following company policy, who's liable?&lt;/p&gt;

&lt;p&gt;Not morally. Legally.&lt;/p&gt;

&lt;p&gt;Because as AI-generated code scales from 33% to 50% to whatever comes after, and as the bugs scale right alongside it, someone's going to ask that question in front of a judge instead of on Reddit. The answer won't be "the junior developer."&lt;/p&gt;

&lt;p&gt;Companies that figure this out now, that build the infrastructure and review processes and accountability structures before they're forced to, will still be standing when the dust settles. The ones that keep firing juniors for using the tools they were told to use will learn the lesson the expensive way.&lt;/p&gt;

&lt;p&gt;That junior deserved a better process. Not a cardboard box.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Has your company forced AI coding tools on the team? What does the review process look like? Does a review process even exist?&lt;/strong&gt; Drop a comment below. The more engineers talk openly about what's actually happening inside these orgs, the harder it gets for leadership to pretend the current approach is working.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>discuss</category>
      <category>programming</category>
    </item>
    <item>
      <title>Cursor 3 Is Silently Deleting Code, Draining Wallets, and Swapping Models Behind Closed Doors</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 05 Apr 2026 17:20:08 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/cursor-3-is-silently-deleting-code-draining-wallets-and-swapping-models-behind-closed-doors-24cn</link>
      <guid>https://dev.to/gabrielanhaia/cursor-3-is-silently-deleting-code-draining-wallets-and-swapping-models-behind-closed-doors-24cn</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;gabrielanhaia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The AI coding tool that promised to 10x developer productivity just quietly deleted an afternoon's work. Then it charged for the privilege.&lt;/p&gt;

&lt;p&gt;That's not hyperbole. That's a Tuesday for Cursor 3 users who've been filing bug reports, comparing billing screenshots, and watching their model settings rearrange themselves overnight. What shipped as an "agent-first" IDE looks more like a trust-destruction machine wearing a VS Code skin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug That Should've Blocked the Release
&lt;/h2&gt;

&lt;p&gt;Picture this. A developer writes code. Cursor's agent mode makes edits. Everything checks out. Then the developer spots something minor, clicks "Fix in Chat," and the agent takes care of it.&lt;/p&gt;

&lt;p&gt;Except when they flip back to their file, their previous changes are gone.&lt;/p&gt;

&lt;p&gt;Not flagged. Not conflicting. Not sitting in some recovery panel. Gone. The file quietly reverted to its pre-agent state while nobody was looking.&lt;/p&gt;

&lt;p&gt;The root cause sits in a conflict between the Agent Review Tab and the main editor state. When both are active and a developer triggers "Fix in Chat," Cursor overwrites the file with the older version. No diff notification. No undo prompt. The edits vanish like they never happened.&lt;/p&gt;

&lt;p&gt;Here's the kicker: this isn't some obscure edge case requiring a bizarre reproduction sequence. The "Fix in Chat" button sits right there in the UI. The Agent Review Tab opens automatically. Using both in sequence is the workflow Cursor's own interface encourages. It's the happy path. And it destroys work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A code editor that silently deletes changes has broken the most basic contract between a tool and the person relying on it. Bugs ship. Silent data loss ships differently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Workarounds (That Shouldn't Be Necessary)
&lt;/h3&gt;

&lt;p&gt;These aren't fixes. They're defensive coding against a paid product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Close the Agent Review Tab before clicking "Fix in Chat."&lt;/strong&gt; This is the big one. Manually closing that tab before sending any follow-up prompt to the agent prevents the revert. Yes, that means avoiding a core feature to stop the product from eating code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit like a paranoid junior dev.&lt;/strong&gt; Small, frequent git commits become restore points. When Cursor silently reverts something, &lt;code&gt;git diff&lt;/code&gt; reveals what vanished and &lt;code&gt;git checkout&lt;/code&gt; brings it back.&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;# Run this in a separate terminal to catch silent reverts&lt;/span&gt;
watch &lt;span class="nt"&gt;-n&lt;/span&gt; 5 &lt;span class="s1"&gt;'git diff --stat'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Keep an external diff monitor running.&lt;/strong&gt; A terminal with &lt;code&gt;git diff --stat&lt;/code&gt; on a loop or a tool like &lt;code&gt;delta&lt;/code&gt; watching for unexpected file changes acts as a canary. If a file suddenly shows modifications nobody made, something went wrong.&lt;/p&gt;

&lt;p&gt;
  Disable the Agent Review Tab entirely
  &lt;p&gt;Some users report that disabling the review tab through settings prevents the revert bug altogether. The tradeoff: losing a significant chunk of Cursor's agent workflow. That's a functionality sacrifice no paying customer should have to make, but it works.&lt;/p&gt;

&lt;p&gt;Check &lt;strong&gt;Settings &amp;gt; Features &amp;gt; Agent&lt;/strong&gt; and look for review-related toggles. Naming varies between versions.&lt;/p&gt;



&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent Mode and the $2,000 Week
&lt;/h2&gt;

&lt;p&gt;Old Composer was predictable. One request, one response, done. Agent mode works on a completely different cost model. It chains multiple calls together: planning steps, executing them, reviewing results, iterating. Each link in that chain burns tokens.&lt;/p&gt;

&lt;p&gt;Developers reported $2,000 in charges across just two days. Not two months. Not even two weeks. Two days. And they weren't running some absurd stress test. They were using agent mode the way Cursor's own demos showed it being used.&lt;/p&gt;

&lt;p&gt;Making it worse, Cursor has been quietly tightening request limits each quarter. What started as generous early-adopter allocations keeps shrinking, shoving more users into overage billing territory.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Actually Costs: Tool Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cursor Pro&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Windsurf&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20/mo&lt;/td&gt;
&lt;td&gt;$20/mo (Max plan)&lt;/td&gt;
&lt;td&gt;$10–19/mo&lt;/td&gt;
&lt;td&gt;$15/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent/agentic mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Included (capped)&lt;/td&gt;
&lt;td&gt;Terminal-native agent&lt;/td&gt;
&lt;td&gt;Copilot Chat&lt;/td&gt;
&lt;td&gt;Cascade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overage model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pay-per-request past cap&lt;/td&gt;
&lt;td&gt;API usage billing&lt;/td&gt;
&lt;td&gt;Flat rate, no overage&lt;/td&gt;
&lt;td&gt;Tiered limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typical heavy-use week&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$100–$2,000+&lt;/td&gt;
&lt;td&gt;$50–$200&lt;/td&gt;
&lt;td&gt;$10–$19 (fixed)&lt;/td&gt;
&lt;td&gt;$15–$60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost predictability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium-High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hard spending cap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Buried in dashboard&lt;/td&gt;
&lt;td&gt;API budget controls&lt;/td&gt;
&lt;td&gt;N/A (flat rate)&lt;/td&gt;
&lt;td&gt;Tier-based&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That "$2,000+" isn't a typo or a worst-case scenario invented for drama. It's a real number from real users doing real work. Cursor is the only tool on this list where weekly costs can spike by an order of magnitude without warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locking Down the Billing
&lt;/h3&gt;

&lt;p&gt;Cursor doesn't make cost controls obvious. Here's where to find them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;web dashboard&lt;/strong&gt; (not editor settings). Go to &lt;strong&gt;Settings &amp;gt; Billing&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Look for usage caps. Set a hard monthly maximum if one exists&lt;/li&gt;
&lt;li&gt;Monitor the &lt;strong&gt;Usage&lt;/strong&gt; tab daily for at least the first two weeks of agent mode&lt;/li&gt;
&lt;li&gt;For routine edits, skip agent mode entirely. Standard autocomplete and inline chat handle most tasks without triggering multi-step chains
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pro tip: Set a calendar reminder to check billing every 48 hours.
Annoying? Yes. Cheaper than the alternative? Absolutely.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deeper problem isn't just that agent mode costs more. It's that costs are unpredictable. A simple refactor might take three agent steps one day and eleven the next, depending on how the planner interprets the codebase. With autocomplete, costs track keystrokes. With agent mode, costs track the model's internal reasoning, which nobody can forecast.&lt;/p&gt;

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

&lt;p&gt;TechCrunch reported in early 2026 that Cursor's Composer 2 model was built on top of Moonshot AI's Kimi 2.5. Moonshot AI is a Chinese AI company. Plenty of excellent research comes from Chinese labs. That's not the issue.&lt;/p&gt;

&lt;p&gt;The issue is that Cursor told nobody.&lt;/p&gt;

&lt;p&gt;Developers paying $20–$40/month had every reason to believe they knew what models were processing their code. Cursor's settings page shows model names. Users pick between Claude, GPT-4, and other options. What they didn't pick, because they were never given the choice, was a fine-tuned version of Kimi 2.5 running under a different label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For compliance teams, this is a five-alarm fire.&lt;/strong&gt; Organizations that approved Cursor based on Anthropic and OpenAI model usage might have a very different answer for Moonshot AI. Where do prompts go when Composer 2 handles them? Through Cursor's servers only, or do they touch Moonshot AI infrastructure? Without disclosure, there's no way to audit.&lt;/p&gt;

&lt;p&gt;If a company will quietly swap out the model powering its flagship feature, what else changes without announcement? That question sits at the center of every trust conversation around Cursor right now. And the company still hasn't answered it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Smaller Cuts That Bleed Trust
&lt;/h2&gt;

&lt;p&gt;Headlines go to the silent revert and the billing explosions. But Cursor 3 shipped with a whole collection of paper cuts that make the trust problem feel systemic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Review Next File" crash.&lt;/strong&gt; Clicking this button in the agent review flow crashes the application. Not sometimes. Reliably. Users lose their in-progress review state and, depending on timing, uncommitted changes too. A core navigation button in a core feature, and it crashes the app. That shipped to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model settings resetting to "auto."&lt;/strong&gt; Developers have opened Cursor to find their hand-picked model preferences silently flipped back to "auto." Someone who chose Claude 3.5 Sonnet for its coding strengths could be routed to whatever Cursor's auto-selection algorithm picks. Given the Moonshot AI situation, a setting that changes itself carries implications far beyond "annoying UI bug."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compound failure.&lt;/strong&gt; Any single one of these is forgivable. Software ships bugs. Billing gets messy. Partnerships evolve. But when all of it lands simultaneously, in the same release, on paying customers, the pattern stops looking like growing pains and starts looking like a product that shipped months before it was ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify Model Settings Right Now
&lt;/h2&gt;

&lt;p&gt;This takes sixty seconds and might save hours of confusion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1: Open Cursor &amp;gt; Settings (gear icon, bottom left)
Step 2: Navigate to Models or AI Settings
Step 3: Check the Default Model dropdown
Step 4: If it says "auto" and that's not what was set, it was reset
Step 5: Explicitly select the preferred model. Screenshot the selection.
Step 6: Check again in 48 hours. If it reverted to "auto," the bug is active.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
  For teams: a more thorough audit process
  &lt;ul&gt;
&lt;li&gt;Add model-setting verification to the developer onboarding checklist&lt;/li&gt;
&lt;li&gt;Every dev on a Cursor Business plan should verify settings weekly until the reset bug is confirmed fixed&lt;/li&gt;
&lt;li&gt;Screenshot the settings page &lt;strong&gt;after every Cursor update&lt;/strong&gt; (updates are a common trigger for resets)&lt;/li&gt;
&lt;li&gt;Look for a separate &lt;strong&gt;Composer Model&lt;/strong&gt; or &lt;strong&gt;Agent Model&lt;/strong&gt; setting, distinct from the chat model. Composer is where the Moonshot-sourced model reportedly runs&lt;/li&gt;
&lt;li&gt;If the org has compliance requirements around AI providers, those dated screenshots create an audit trail showing what was configured versus what actually ran&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;p&gt;For anyone doing this check right now: also look at the &lt;strong&gt;Composer Model&lt;/strong&gt; or &lt;strong&gt;Agent Model&lt;/strong&gt; setting if it exists separately from the chat model setting. That's where the Moonshot AI-sourced model reportedly operates, and it might be configured differently from what the main settings page suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cursor Is Breaking Things
&lt;/h2&gt;

&lt;p&gt;This isn't carelessness. It's panic.&lt;/p&gt;

&lt;p&gt;Claude Code captured roughly 54% of the AI-assisted coding market. That number reshaped the competitive map overnight. Anthropic's approach turned out to be what a huge segment of developers actually wanted: a terminal-native agent that works with existing tools rather than replacing the editor.&lt;/p&gt;

&lt;p&gt;Cursor's response was a hard pivot toward "agent orchestrator" territory. That pivot is Cursor 3. Agent-first design, multi-step workflows, the Review Tab, Composer 2. All of it is an attempt to compete with Claude Code's agentic approach from inside a VS Code fork.&lt;/p&gt;

&lt;p&gt;On a whiteboard, the strategy makes sense. In production, it shipped half-baked. Features that needed three more months of testing went live because the competitive window felt like it was closing. Power users built Cursor's reputation through word-of-mouth. Those same power users are now the ones absorbing the damage.&lt;/p&gt;

&lt;p&gt;Several long-time Cursor advocates have publicly described feeling abandoned as the product moves away from the refined autocomplete-and-chat experience that made it popular. They signed up for the best AI-powered code editor. What they got was an agent orchestrator with a reliability problem. Those are different products with different quality bars.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Needs to Happen
&lt;/h2&gt;

&lt;p&gt;Cursor has a narrow window to recover.&lt;/p&gt;

&lt;p&gt;The silent revert bug needs a fix measured in days, not sprint cycles. The billing model needs a hard spending cap that's visible &lt;em&gt;before&lt;/em&gt; charges pile up. The Moonshot AI sourcing needs a retroactive disclosure: what model runs where, where code data flows, and who has access.&lt;/p&gt;

&lt;p&gt;Most importantly, model settings need to stay where users put them. When a developer selects Claude Sonnet, that selection should hold until the developer changes it. Not "auto." Not quietly rerouted. Locked.&lt;/p&gt;

&lt;p&gt;The developer tools market has never offered more alternatives. GitHub Copilot keeps iterating. Windsurf is gaining traction on simplicity and price. Claude Code is expanding its terminal-native approach. Switching costs for an editor extension hover near zero.&lt;/p&gt;

&lt;p&gt;Cursor built its user base on being the AI editor that "just worked." Version 3 has put that reputation in critical condition. The bugs are fixable. The billing model is adjustable. Trust, once broken, rebuilds on a much longer timeline.&lt;/p&gt;

&lt;p&gt;For now, the practical advice is blunt: commit constantly, verify model settings on a schedule, monitor billing like it's a production dashboard, and keep a backup editor configured and ready. No developer tool should require that level of defensive behavior from its paying users.&lt;/p&gt;

&lt;p&gt;But here we are.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>devtools</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>A North Korean Backdoor Lived Inside Axios for 3 Hours. Millions of Pipelines Pulled It.</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 05 Apr 2026 17:19:09 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/a-north-korean-backdoor-lived-inside-axios-for-3-hours-millions-of-pipelines-pulled-it-e5j</link>
      <guid>https://dev.to/gabrielanhaia/a-north-korean-backdoor-lived-inside-axios-for-3-hours-millions-of-pipelines-pulled-it-e5j</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;gabrielanhaia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One hundred million weekly downloads. One stolen token. One backdoor.
&lt;/h2&gt;

&lt;p&gt;Axios &lt;code&gt;1.14.1&lt;/code&gt; dropped on March 31, 2026. So did &lt;code&gt;0.30.4&lt;/code&gt;. Both looked like routine patch releases. Neither came from the axios team.&lt;/p&gt;

&lt;p&gt;They came from North Korea.&lt;/p&gt;

&lt;p&gt;For roughly three hours, every &lt;code&gt;npm install&lt;/code&gt; that resolved to either version pulled in a dependency called &lt;code&gt;plain-crypto-js&lt;/code&gt;. That dependency deployed WAVESHAPER.V2, a multi-platform backdoor built to survive deletion, phone home to a command-and-control server, and hand a remote shell to the attacker. Windows, macOS, Linux. All covered. All weaponized.&lt;/p&gt;

&lt;p&gt;Google's Threat Intelligence Group attributed the operation to &lt;strong&gt;UNC1069&lt;/strong&gt;, a North Korea-nexus cluster active since 2018. Not a teenager with a typosquat. A government-funded team with a track record measured in billions of stolen dollars.&lt;/p&gt;

&lt;p&gt;Three hours sounds short. At axios's scale, it wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Single Token Burned the Supply Chain
&lt;/h2&gt;

&lt;p&gt;The attack surface was embarrassingly small. One npm authentication token, belonging to one axios maintainer, in the hands of one threat actor. That was enough.&lt;/p&gt;

&lt;p&gt;No MFA bypass was reported. No elaborate zero-day exploit chain. The working assumption across every post-incident analysis is that the token itself was the target. Stolen credentials, phished session, compromised development machine — the exact vector hasn't been publicly confirmed. The outcome, though, is crystal clear.&lt;/p&gt;

&lt;p&gt;Here's the hour-by-hour damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;March 31, early hours (UTC):&lt;/strong&gt; The attacker authenticates to the npm registry using the compromised token. Two new versions publish almost simultaneously. Version &lt;code&gt;1.14.1&lt;/code&gt; for the current 1.x release line. Version &lt;code&gt;0.30.4&lt;/code&gt; for the legacy 0.x line. Both add a single new dependency: &lt;code&gt;plain-crypto-js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The name was chosen with care. It reads like a boring crypto utility. The kind of package that shows up in a diff and gets waved through because nobody wants to investigate a transitive dependency at 2 AM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minutes later:&lt;/strong&gt; CI/CD pipelines worldwide start resolving the new versions. Any project using caret ranges like &lt;code&gt;^1.14.0&lt;/code&gt; or lacking a committed lockfile gets the poisoned release automatically. No user action required. No prompt. No confirmation dialog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;~2.5 hours after publication:&lt;/strong&gt; Security researchers flag the anomaly. The compromised versions get yanked from npm. The maintainer's account is locked and recovered. Advisories begin circulating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Following days:&lt;/strong&gt; Microsoft, Google, and Tenable publish formal analyses. Google ties the operation to UNC1069 with high confidence.&lt;/p&gt;

&lt;p&gt;The window closed fast. What happened inside it didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of the Payload: What &lt;code&gt;plain-crypto-js&lt;/code&gt; Actually Executed
&lt;/h2&gt;

&lt;p&gt;Calling this malware "sophisticated" undersells the engineering. It wasn't a single script grabbing environment variables and posting them to a webhook. It was a staged deployment pipeline — built by attackers who clearly understood how developers work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Fingerprinting the target
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;postinstall&lt;/code&gt; script fires the moment npm finishes writing the package to disk. It reads &lt;code&gt;process.platform&lt;/code&gt; and &lt;code&gt;process.arch&lt;/code&gt;, then selects the appropriate payload. Three operating systems. Two CPU architectures for macOS (x64 and arm64). Someone compiled, tested, and packaged binaries for every platform a working developer might use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified reconstruction of the platform check&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;os&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 'win32', 'darwin', 'linux'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// 'x64', 'arm64'&lt;/span&gt;
&lt;span class="c1"&gt;// Selects and fetches the correct binary for the host&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stage 2: Binary drop
&lt;/h3&gt;

&lt;p&gt;Based on fingerprinting results, the script downloads a platform-native binary from an external server. It writes the binary to a hidden path inside &lt;code&gt;node_modules&lt;/code&gt; and executes it. Quick. Quiet. Buried under thousands of other files that nobody ever manually inspects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: WAVESHAPER.V2 takes the machine
&lt;/h3&gt;

&lt;p&gt;The binary is the real weapon. Google's analysts designated it &lt;strong&gt;WAVESHAPER.V2&lt;/strong&gt;, and its capabilities read like a pentester's wish list:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistence.&lt;/strong&gt; It copies itself outside the project directory before doing anything else. Deleting &lt;code&gt;node_modules&lt;/code&gt; doesn't touch it. Running &lt;code&gt;npm install&lt;/code&gt; with a clean version doesn't remove it. The backdoor relocates to OS-specific persistence locations — &lt;code&gt;LaunchAgents&lt;/code&gt; on macOS, &lt;code&gt;systemd&lt;/code&gt; user services on Linux, startup folders and scheduled tasks on Windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command and control.&lt;/strong&gt; It opens a reverse connection to an attacker-controlled C2 server and maintains the channel. Remote shell access. Full.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exfiltration.&lt;/strong&gt; Environment variables, SSH keys, cloud credential files, browser session tokens, cryptocurrency wallet data. Everything accessible to the user account that ran &lt;code&gt;npm install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Survival.&lt;/strong&gt; Even a factory reset of the project directory leaves the backdoor intact. It's living in the home directory. On the system. Not in the dependency tree.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Removing &lt;code&gt;node_modules&lt;/code&gt; after installing a compromised version does nothing. The host machine requires a full incident response investigation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What UNC1069 was harvesting
&lt;/h3&gt;

&lt;p&gt;North Korean cyber operations aren't about espionage in the traditional sense. They're about money.&lt;/p&gt;

&lt;p&gt;The UN estimated DPRK-linked groups stole over $3 billion in cryptocurrency between 2017 and 2023. The pace has accelerated since. UNC1069's target list follows the pattern: crypto wallet keys, AWS/GCP/Azure tokens sitting in &lt;code&gt;~/.aws/credentials&lt;/code&gt; or environment variables, SSH private keys for lateral movement into other systems, and CI/CD secrets that open doors to entire organizations.&lt;/p&gt;

&lt;p&gt;A developer laptop with cloud access and deployment permissions is worth more to this group than most corporate database servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Nightmare: This Wasn't a Solo Operation
&lt;/h2&gt;

&lt;p&gt;The axios compromise would be alarming enough on its own. It wasn't on its own.&lt;/p&gt;

&lt;p&gt;Between &lt;strong&gt;March 19 and March 27&lt;/strong&gt; — days before the axios attack — a separate North Korea-linked group tracked as &lt;strong&gt;TeamPCP (UNC6780)&lt;/strong&gt; executed a parallel campaign through a completely different vector. They didn't steal npm tokens. They exploited GitHub Actions workflows and PyPI packages.&lt;/p&gt;

&lt;p&gt;Their target list is almost funny in a gallows-humor kind of way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trivy&lt;/strong&gt;, Aqua Security's vulnerability scanner. The tool organizations use to detect compromised dependencies was itself compromised. &lt;strong&gt;KICS&lt;/strong&gt; (Keeping Infrastructure as Code Secure) got the same treatment. A security tool, backdoored. &lt;strong&gt;LiteLLM&lt;/strong&gt;, a popular LLM proxy library, was hit. So was &lt;strong&gt;Telnyx&lt;/strong&gt;, a cloud communications platform.&lt;/p&gt;

&lt;p&gt;TeamPCP deployed &lt;strong&gt;SANDCLOCK&lt;/strong&gt;, a credential stealer purpose-built for CI/CD environments. It harvested cloud tokens, API keys, and pipeline secrets from build runners.&lt;/p&gt;

&lt;p&gt;Two groups. Two attack vectors. Multiple high-value targets. All within twelve days.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether UNC1069 and UNC6780 coordinated directly or simply operated under the same strategic directive from Pyongyang is unknown. The result is identical either way: a sustained, multi-pronged assault on the open-source supply chain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Are You Affected? Find Out Right Now.
&lt;/h2&gt;

&lt;p&gt;Stop skimming. Run these commands. The rest of the article isn't going anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the lockfile for the malicious dependency
&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;# npm projects&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js"&lt;/span&gt; package-lock.json

&lt;span class="c"&gt;# yarn projects&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js"&lt;/span&gt; yarn.lock

&lt;span class="c"&gt;# pnpm projects&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js"&lt;/span&gt; pnpm-lock.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any match means the compromised version was installed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify your current axios version
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;ls &lt;/span&gt;axios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output shows exactly which version sits in the dependency tree right now. If it reads &lt;code&gt;1.14.1&lt;/code&gt; or &lt;code&gt;0.30.4&lt;/code&gt;, the machine that ran the install needs investigation. Not just the project. The machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the npm cache
&lt;/h3&gt;

&lt;p&gt;Even if the current version is clean, the cache remembers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm cache &lt;span class="nb"&gt;ls &lt;/span&gt;2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"axios"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run npm audit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advisory for the compromised versions should surface here if the dependency tree ever referenced them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hunt for WAVESHAPER.V2 persistence artifacts
&lt;/h3&gt;

&lt;p&gt;
  If you confirmed a compromised version was installed, check these locations
  &lt;p&gt;&lt;strong&gt;macOS:&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;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; ~/Library/LaunchAgents/
find ~/.local &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-newer&lt;/span&gt; /tmp 2&amp;gt;/dev/null
find /tmp &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;".*"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Linux:&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;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; ~/.config/systemd/user/ 2&amp;gt;/dev/null
find ~/.local &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-newer&lt;/span&gt; /tmp 2&amp;gt;/dev/null
find /tmp &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;".*"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows (PowerShell):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ScheduledTask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-gt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-30"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;APPDATA&lt;/span&gt;&lt;span class="s2"&gt;\Microsoft\Windows\Start Menu\Programs\Startup"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any unfamiliar files created on or after March 31, 2026 in these locations warrant immediate escalation to a security team.&lt;/p&gt;



&lt;br&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't forget CI/CD
&lt;/h3&gt;

&lt;p&gt;This is the check most teams will skip. Don't.&lt;/p&gt;

&lt;p&gt;If a CI pipeline ran &lt;code&gt;npm install&lt;/code&gt; (not &lt;code&gt;npm ci&lt;/code&gt;) during the March 31 window without a pinned lockfile, the build runner may have executed the backdoor. Ephemeral runners like GitHub Actions hosted agents are lower risk because they're destroyed after each job. Self-hosted runners, Jenkins boxes, and long-lived build servers are a completely different situation.&lt;/p&gt;

&lt;p&gt;Review outbound network logs from those machines for the March 31 timeframe. Any unexpected connections to unfamiliar IPs during or after the window should be treated as indicators of compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Every Team Should Do This Week
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Today
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Run every detection command above.&lt;/strong&gt; Every project. Every CI environment. Every build server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch CI pipelines from &lt;code&gt;npm install&lt;/code&gt; to &lt;code&gt;npm ci&lt;/code&gt;.&lt;/strong&gt; This single change would have blocked the axios compromise entirely for any project with a committed lockfile. &lt;code&gt;npm ci&lt;/code&gt; installs exactly what the lockfile specifies and fails hard on mismatches.&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;# Replace this in every CI config:&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# With this:&lt;/span&gt;
npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pin dependency versions.&lt;/strong&gt; Caret ranges (&lt;code&gt;^1.14.0&lt;/code&gt;) and tilde ranges (&lt;code&gt;~1.14.0&lt;/code&gt;) exist for convenience. They also exist as an attack surface. Exact versions in &lt;code&gt;package.json&lt;/code&gt; plus a committed lockfile equals a dramatically smaller blast radius.&lt;/p&gt;

&lt;h3&gt;
  
  
  This week
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Audit and rotate all npm tokens.&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;npm token list
npm token revoke &amp;lt;token-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Revoke anything unrecognized. Rotate everything else. If anyone on the team maintains a public package, enforce 2FA on their npm account today, not tomorrow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review GitHub Actions workflows&lt;/strong&gt; for the patterns TeamPCP exploited. Workflows using &lt;code&gt;pull_request_target&lt;/code&gt; with a checkout of the PR branch allow external contributors to run arbitrary code with write permissions. That's the exact mechanism UNC6780 used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluate a registry proxy.&lt;/strong&gt; Tools like Verdaccio, Artifactory, or npm Enterprise can allowlist specific package versions before they reach developer machines. It's an extra layer that costs time to configure and saves everything when it matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ongoing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Run &lt;code&gt;npm audit&lt;/code&gt; as a blocking CI step.&lt;/strong&gt; Known vulnerabilities should fail the build. No exceptions. No &lt;code&gt;--force&lt;/code&gt; to skip warnings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscribe to security feeds.&lt;/strong&gt; GitHub's advisory database, npm security advisories, and Google Threat Intelligence all published within 48 hours of the axios incident. Teams that monitor these feeds had time to react. Teams that don't monitor them found out from Twitter, days later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why npm Keeps Getting Hit
&lt;/h2&gt;

&lt;p&gt;The npm registry is built on a trust model designed for a different era. Anyone with an account can publish. Anyone with a maintainer token can push a new version of an existing package. There's no mandatory code review gate. No build reproducibility requirement. No quarantine period between publication and global availability.&lt;/p&gt;

&lt;p&gt;Compare that to Linux distribution package management, where maintainer committees review submissions and reproducible builds can be independently verified. Or mobile app stores, where multi-day review processes and cryptographic signing are table stakes.&lt;/p&gt;

&lt;p&gt;npm has a username, a password, and an optional TOTP code. That's the entire barrier between an attacker and one hundred million weekly installs.&lt;/p&gt;

&lt;p&gt;Improvements exist. Provenance attestation. Granular access tokens. 2FA support. All voluntary. All opt-in. Adoption rates are growing but remain far from universal.&lt;/p&gt;

&lt;p&gt;The gap between npm's trust architecture and the actual threat environment is staggering. Nation-state actors have done the math. Compromising a single developer account on a package registry provides simultaneous access to thousands of downstream organizations. The return on investment is extraordinary, and North Korea has stronger financial incentive than almost any other actor to keep exploiting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;A state-sponsored group compromised one of the most widely installed packages on the planet. The attack was caught in under three hours. That's genuinely fast, compared to supply chain incidents that take weeks or months to surface.&lt;/p&gt;

&lt;p&gt;Fast wasn't fast enough.&lt;/p&gt;

&lt;p&gt;Every developer who ran &lt;code&gt;npm install&lt;/code&gt; without a pinned lockfile during that window was exposed. Every CI pipeline pulling fresh dependencies without &lt;code&gt;npm ci&lt;/code&gt; was exposed. Every self-hosted build server that cached the compromised version might still be running a backdoor right now, five days later, with nobody aware.&lt;/p&gt;

&lt;p&gt;Treating dependency management as a convenience function rather than a security function is the root cause. Pin versions. Commit lockfiles. Use &lt;code&gt;npm ci&lt;/code&gt; in automation. Audit regularly. Monitor advisories. Investigate machines that pulled affected versions, not just the projects.&lt;/p&gt;

&lt;p&gt;The axios backdoor lived for under three hours. The next one might not get caught that quickly.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>npm</category>
      <category>node</category>
    </item>
    <item>
      <title>GitHub Starts Training AI on Your Private Code April 24 — Here's How to Stop It</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 05 Apr 2026 17:17:33 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/github-starts-training-ai-on-your-private-code-april-24-heres-how-to-stop-it-19d3</link>
      <guid>https://dev.to/gabrielanhaia/github-starts-training-ai-on-your-private-code-april-24-heres-how-to-stop-it-19d3</guid>
      <description>&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;gabrielanhaia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Nineteen Days
&lt;/h2&gt;

&lt;p&gt;On March 25, 2026, GitHub published a blog post. Most developers didn't see it. The ones who did wish they hadn't needed to.&lt;/p&gt;

&lt;p&gt;Buried under several paragraphs of cheerful corporate language about "improving the Copilot experience," one detail stood out: starting &lt;strong&gt;April 24, 2026&lt;/strong&gt;, GitHub will use Copilot interaction data from Free, Pro, and Pro+ users to train its AI models. Not "might use." Not "is exploring." Will use. Default: on.&lt;/p&gt;

&lt;p&gt;The Hacker News thread went exactly how anyone would expect. The Register picked it up. And now there are roughly &lt;strong&gt;19 days left&lt;/strong&gt; before this kicks in. Every affected user who doesn't explicitly opt out gets enrolled automatically.&lt;/p&gt;

&lt;p&gt;That's bad enough. The specifics are worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Interaction Data" Actually Means
&lt;/h2&gt;

&lt;p&gt;GitHub chose the term "interaction data" carefully. It sounds harmless. Benign, even. Like telemetry about button clicks. It isn't.&lt;/p&gt;

&lt;p&gt;Here's what falls under that label:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompts and instructions.&lt;/strong&gt; Every question typed into Copilot Chat. Every inline comment written to trigger a suggestion. A developer typing "refactor this auth handler to support OAuth2 PKCE flow" — that's collected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code snippets sent as context.&lt;/strong&gt; This is the big one. When Copilot generates suggestions, it reads surrounding code for context. That surrounding code gets sent to GitHub's API as part of the request. Code from private repositories. Collected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated outputs.&lt;/strong&gt; Every suggestion Copilot produces, accepted or rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behavioral signals.&lt;/strong&gt; Which suggestions were accepted, which were dismissed, how developers modified the output after accepting it. A detailed log of how humans interact with AI-generated code.&lt;/p&gt;

&lt;p&gt;Here's what a single interaction looks like from a data collection perspective:&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;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add JWT validation with RS256 to this endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"context_snippets"&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="s2"&gt;"class AuthService:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    def __init__(self, secret_key, issuer)..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"from app.models import User, Session, TokenBlacklist..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ALLOWED_ORIGINS = ['https://internal-dashboard.company.com']..."&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;"suggestion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def validate_token(self, token: str) -&amp;gt; dict:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    try:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;        payload = jwt.decode(token, self.public_key, algorithms=['RS256'])..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"modified_after_acceptance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Notice what's in those context snippets. Internal class names. Import paths that reveal project structure. An internal URL. None of this was pushed to a public repo. It got hoovered up because a developer used the autocomplete in their editor.&lt;/p&gt;

&lt;p&gt;One interaction might expose a handful of lines. A full workday of Copilot usage? That's a different story.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;40 Copilot interactions per day
× ~200-500 lines of surrounding context each
= 8,000-20,000 lines of private code per day
× 20 working days per month
= 160,000-400,000 lines per month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That math isn't theoretical. It's what happens when someone actually uses Copilot the way GitHub wants them to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GitHub Says It Won't Touch
&lt;/h2&gt;

&lt;p&gt;Credit where it's due: GitHub has been specific about the boundaries. The distinction is real, even if it's thinner than the marketing suggests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Private repository content "at rest" is not used for training. GitHub does not crawl private repos and feed them into models.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A developer with a 50,000-line private codebase who only used Copilot on 200 lines means only those 200 lines' worth of context entered the pipeline. The other 49,800 lines stay put.&lt;/p&gt;

&lt;p&gt;GitHub also confirmed that &lt;strong&gt;Business and Enterprise&lt;/strong&gt; plan users are completely excluded. Their data policies haven't changed. This only hits individual-tier accounts: Free, Pro, and Pro+.&lt;/p&gt;

&lt;p&gt;Which creates an absurd situation. A solo developer building a SaaS product on a personal Pro account gets less data protection than a junior dev at a Fortune 500 company using the same tool on the same kind of code. The difference? Corporate lawyers negotiated those Enterprise terms. Individual developers got a blog post and a toggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Opt-Out Walkthrough
&lt;/h2&gt;

&lt;p&gt;This takes about 90 seconds. There's no reason to wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Open the Copilot settings page.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Direct URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/settings/copilot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manual path: GitHub profile icon (top right) → Settings → Copilot (left sidebar).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Find the training data toggle.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look for a setting labeled:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Allow GitHub to use my data for product improvements"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's on the main Copilot settings page under the data usage section. Simple toggle switch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Turn it off.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Flip the toggle to disabled. The page should auto-save. If there's a save button, click it. Verify it shows as disabled before navigating away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Verify via API.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;UI toggles are great. API confirmation is better. Run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_GITHUB_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-GitHub-Api-Version: 2022-11-28"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.github.com/user/copilot &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'{ ide_chat: .copilot_ide_chat, model_training: .copilot_model_training_opt_in }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_GITHUB_TOKEN&lt;/code&gt; with a personal access token that has the &lt;code&gt;read:user&lt;/code&gt; scope. The &lt;code&gt;copilot_model_training_opt_in&lt;/code&gt; field should return &lt;code&gt;false&lt;/code&gt;. If it returns &lt;code&gt;true&lt;/code&gt;, the opt-out didn't stick — go back and try again.&lt;/p&gt;

&lt;p&gt;Don't have &lt;code&gt;jq&lt;/code&gt; installed? The raw JSON output works fine too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_GITHUB_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/vnd.github+json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-GitHub-Api-Version: 2022-11-28"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.github.com/user/copilot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search the output for &lt;code&gt;copilot_model_training_opt_in&lt;/code&gt; and confirm it's &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Check organization settings.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Anyone who administers a GitHub organization on a Free or Pro plan should also check org-level settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/organizations/YOUR-ORG/settings/copilot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Organization admins can enforce the opt-out for all members. Worth doing, especially for open source projects where contributors on personal accounts might not update their own settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who's Affected and Who Isn't
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Affected?&lt;/th&gt;
&lt;th&gt;Action Needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Free (with Copilot Free)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Opt out manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Pro&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Opt out manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Pro+&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Opt out manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Team&lt;/td&gt;
&lt;td&gt;Possibly&lt;/td&gt;
&lt;td&gt;Admin should verify org settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot Business&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Existing data policies unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot Enterprise&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Existing data policies unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tiering tells on itself. Enterprise customers have legal teams that would shred the contract over a default opt-in to training. Individual developers don't have legal teams. They have Reddit threads and strongly worded blog posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "We're Not Reading Your Repos" Problem
&lt;/h2&gt;

&lt;p&gt;GitHub's position boils down to: "We're only using interaction data, not your repository content." Technically accurate. Practically misleading.&lt;/p&gt;

&lt;p&gt;The interaction data &lt;em&gt;contains&lt;/em&gt; code from those repositories. Every snippet sent as context originated from a repo the developer intended to keep private. Every prompt that references internal architecture. Every generated output that builds on proprietary logic. It all came from somewhere, and that somewhere was a private repo.&lt;/p&gt;

&lt;p&gt;The distinction between "we didn't read your private repo" and "we read the parts of your private repo that flowed through our tool" is real but razor-thin. Especially at scale.&lt;/p&gt;

&lt;p&gt;Think about what a typical Copilot power user generates over a month. Tens of thousands of lines of private code fragments sitting in GitHub's "interaction data" corpus. Nobody pushed those lines to a public repo. Nobody checked a box saying "use this for training." The consent happened retroactively, via a default-on toggle that most users will never see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Supply Chain Angle Nobody's Talking About
&lt;/h2&gt;

&lt;p&gt;Here's where things get genuinely messy.&lt;/p&gt;

&lt;p&gt;A freelancer uses their personal GitHub Pro account. They work on three client projects, all in private repos. They use Copilot constantly because it makes them faster and clients don't care how the code gets written, just that it works.&lt;/p&gt;

&lt;p&gt;Starting April 24, fragments of every client's codebase enter the training pipeline. The freelancer might not even know. The clients definitely don't. No NDA was violated intentionally. The developer just used the same IDE plugin they always use.&lt;/p&gt;

&lt;p&gt;Now scale that to regulated industries. A contractor doing healthcare work uses their personal Copilot account. Internal API naming conventions that hint at data models, database schemas with field names that reference PHI categories, authentication patterns specific to HIPAA-compliant systems — all of that can end up in interaction data. The contractor didn't do anything wrong. They just didn't opt out of a setting they didn't know existed.&lt;/p&gt;

&lt;p&gt;The liability questions here aren't hypothetical anymore. They're calendar events.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Developer Community Got Right (and Wrong)
&lt;/h2&gt;

&lt;p&gt;The backlash split into predictable camps.&lt;/p&gt;

&lt;p&gt;Some developers called it a bait-and-switch. They adopted Copilot under one set of data policies, integrated it into daily workflows, and now those policies are changing underneath them. This is a fair reading. The terms changed. The habits built on the old terms didn't.&lt;/p&gt;

&lt;p&gt;Others argued that interaction data is fundamentally different from repository access and that AI training on usage patterns makes the tool better for everyone. Also fair, but it assumes anonymization is bulletproof and that code fragments aren't reconstructible. That's an assumption doing a lot of heavy lifting.&lt;/p&gt;

&lt;p&gt;The most interesting critique came from developers asking why code is treated differently from other creative work. When Spotify uses listening data to improve recommendations, the algorithm learning someone's taste in music doesn't threaten their career. When GitHub uses coding data to improve Copilot, the improved model generates code that competes with the developer who produced the training data. Users improve the product, and the improved product reduces demand for those same users.&lt;/p&gt;

&lt;p&gt;Nobody's solved that incentive problem. Most companies aren't even acknowledging it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives Worth Knowing About
&lt;/h2&gt;

&lt;p&gt;For developers who want AI coding assistance without the training data question hanging overhead, options exist. None are perfect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosted models.&lt;/strong&gt; Running a local code model through Ollama, LM Studio, or llama.cpp means code never leaves the machine. Quality is improving fast but still falls short on large, context-heavy codebases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copilot Business or Enterprise.&lt;/strong&gt; The "pay more, keep your data" option. GitHub's upsell pitch, working exactly as designed. Cynical but effective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Competitors with no-training policies.&lt;/strong&gt; Some AI coding tools have taken a hard stance against training on user data. Read the actual terms of service, not the landing page. Policies change. This exact GitHub situation proves that point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local inference in the editor.&lt;/strong&gt; Some IDEs are building on-device AI features that never phone home. Still early. The direction matters more than the current quality.&lt;/p&gt;

&lt;p&gt;
  What about open source contributors?
  &lt;br&gt;
For developers working exclusively on public, open-source code, the training data question is less urgent — the code is already public. But interaction data still includes prompts, behavioral signals, and context from private development branches that might not be public yet. Even open source developers should consider opting out if they work on any private branches or repos alongside their public work.&lt;br&gt;


&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Checklist
&lt;/h2&gt;

&lt;p&gt;For anyone who scrolled past everything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Go to &lt;a href="https://github.com/settings/copilot" rel="noopener noreferrer"&gt;github.com/settings/copilot&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Disable "Allow GitHub to use my data for product improvements"&lt;/li&gt;
&lt;li&gt;[ ] Run the API curl command to verify &lt;code&gt;copilot_model_training_opt_in&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] If administering an org, check org-level Copilot settings&lt;/li&gt;
&lt;li&gt;[ ] Tell other developers on the team — especially contractors and freelancers on personal accounts&lt;/li&gt;
&lt;li&gt;[ ] Set a calendar reminder after April 24 to verify the setting didn't reset&lt;/li&gt;
&lt;li&gt;[ ] Review which Copilot features are still enabled and whether the trade-off makes sense&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Default Is the Policy
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth that makes this more than a settings toggle.&lt;/p&gt;

&lt;p&gt;GitHub reported over 150 million developers on the platform. Copilot has millions of active users. Even a generous estimate — say 10% of affected users see the announcement and opt out — leaves millions of developers unknowingly feeding interaction data into training runs. That's not a bug. That's the business model.&lt;/p&gt;

&lt;p&gt;Default settings are policy. Blog posts are plausible deniability. The 30-day window between announcement and enforcement is just long enough to say "we gave people time" and just short enough to ensure most people miss it.&lt;/p&gt;

&lt;p&gt;GitHub built a tool developers genuinely love. Copilot makes people faster. That's not in dispute. What's in dispute is whether a good product earns the right to change data policies retroactively with a default opt-in that most users won't see.&lt;/p&gt;

&lt;p&gt;Nineteen days. Go flip the toggle. Tell someone else to flip theirs.&lt;/p&gt;

</description>
      <category>github</category>
      <category>ai</category>
      <category>security</category>
      <category>discuss</category>
    </item>
    <item>
      <title>52,050 Layoffs Based on Vibes: The Math Behind AI-Driven Workforce Cuts Doesn't Add Up</title>
      <dc:creator>Gabriel Anhaia</dc:creator>
      <pubDate>Sun, 05 Apr 2026 15:21:32 +0000</pubDate>
      <link>https://dev.to/gabrielanhaia/52050-layoffs-based-on-vibes-the-math-behind-ai-driven-workforce-cuts-doesnt-add-up-565h</link>
      <guid>https://dev.to/gabrielanhaia/52050-layoffs-based-on-vibes-the-math-behind-ai-driven-workforce-cuts-doesnt-add-up-565h</guid>
      <description>&lt;p&gt;Harvard Business Review published a piece in early 2026 with a title that should've set off alarm bells across every engineering org on the planet: &lt;em&gt;"Companies Are Laying Off Workers Because of AI's Potential — Not Its Performance."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not performance. &lt;em&gt;Potential.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's the corporate equivalent of demolishing a profitable restaurant because someone heard a rumor about a self-cooking kitchen. Except 52,050 real people lost real jobs in Q1 2026 over this particular rumor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Q1 2026 Scoreboard
&lt;/h2&gt;

&lt;p&gt;Before anyone dismisses this as doomerism, here are the actual numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Q1 2025&lt;/th&gt;
&lt;th&gt;Q1 2026&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total tech layoffs&lt;/td&gt;
&lt;td&gt;~37,000&lt;/td&gt;
&lt;td&gt;52,050&lt;/td&gt;
&lt;td&gt;+40% YoY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Companies citing "AI efficiency"&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;+158%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Measurable AI productivity data shared&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;No change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the important one. Thirty-one companies told Wall Street that AI-driven efficiency justified cutting engineering headcount. Zero of them published internal data showing those efficiency gains actually existed.&lt;/p&gt;

&lt;p&gt;Meta, Google, Amazon, Block, Atlassian, Pinterest, Salesforce. All of them used some variant of "AI productivity" or "automation dividend" on their earnings calls. None of them showed the receipts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 55% Productivity Claim vs. Reality
&lt;/h2&gt;

&lt;p&gt;A number keeps showing up in executive presentations: teams using AI coding assistants produce 40-55% more code per sprint. Sounds great on a slide. It's also the wrong metric, and everybody who's actually shipped software knows it.&lt;/p&gt;

&lt;p&gt;Here's what the engineering workload breakdown looks like in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Activity&lt;/th&gt;
&lt;th&gt;% of Engineer Time&lt;/th&gt;
&lt;th&gt;AI Capability&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writing new code&lt;/td&gt;
&lt;td&gt;~20%&lt;/td&gt;
&lt;td&gt;Strong (boilerplate, CRUD, tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading/understanding existing code&lt;/td&gt;
&lt;td&gt;~25%&lt;/td&gt;
&lt;td&gt;Moderate (summaries, not deep context)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System design &amp;amp; architecture&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging &amp;amp; incident response&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;td&gt;Very weak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code review &amp;amp; collaboration&lt;/td&gt;
&lt;td&gt;~10%&lt;/td&gt;
&lt;td&gt;Superficial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-team coordination&lt;/td&gt;
&lt;td&gt;~10%&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requirements &amp;amp; stakeholder work&lt;/td&gt;
&lt;td&gt;~5%&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AI tools are good at roughly 20% of the job. They're passable at another 25%. They're useless at the remaining 55%. Firing 15-20% of an engineering org because tools got better at the easiest fifth of the work isn't strategy. It's innumeracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code AI Actually Can't Write
&lt;/h2&gt;

&lt;p&gt;The "55% more productive" claim falls apart the second anyone looks at what AI-generated code actually looks like in production systems. Here's a real-world example that illustrates the gap.&lt;/p&gt;

&lt;p&gt;Ask any AI assistant to design a retry mechanism for a distributed payment processing system. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What AI generates (and what juniors accept):
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;payment_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PaymentFailedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payment &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed after 3 attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks clean. Passes linting. Even has exponential backoff. Ship it, right?&lt;/p&gt;

&lt;p&gt;Wrong. A senior engineer would immediately spot what's missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What production actually needs:
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Idempotency key prevents double-charging on retry
&lt;/span&gt;    &lt;span class="n"&gt;idempotency_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_or_create_idempotency_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Check if payment already succeeded (another instance might've
&lt;/span&gt;    &lt;span class="c1"&gt;# completed it while we were retrying)
&lt;/span&gt;    &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;payment_ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PaymentStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COMPLETED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payment &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; already completed, skipping&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;payment_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="c1"&gt;# Gateway timeout must be LESS than our consumer's
&lt;/span&gt;                &lt;span class="c1"&gt;# visibility timeout, or we'll get duplicate processing
&lt;/span&gt;                &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;GATEWAY_TIMEOUT_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;payment_ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dead_letter_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove_if_present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;GatewayTimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# DON'T retry timeouts - the charge might have gone through.
&lt;/span&gt;            &lt;span class="c1"&gt;# Route to manual review queue instead.
&lt;/span&gt;            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;manual_review_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RateLimitError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Respect the gateway's retry-after header, not our own backoff
&lt;/span&gt;            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;TransientError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;MAX_BACKOFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dead_letter_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;traceback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format_exc&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;retry_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PaymentFailedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payment &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed after 3 attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;alert_oncall&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Page someone. Money is involved.
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference between those two blocks is about six production incidents, a possible double-charge, and an angry call from the payments team at 3 AM. AI generated the first version. Only a human who's been burned before writes the second.&lt;/p&gt;

&lt;p&gt;Another example. Try asking an AI to debug this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// "It works fine locally but times out in production 2% of the time"&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueryProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI will suggest adding timeouts, retries, circuit breakers. All wrong. The actual bug? In production, the cache cluster runs across three availability zones. 2% of requests hit a node in a different AZ where a recent network policy change added 800ms of latency. The cache &lt;code&gt;Get&lt;/code&gt; call doesn't fail — it just takes long enough to eat the request's timeout budget before the database call even starts.&lt;/p&gt;

&lt;p&gt;No AI tool figures that out, because it requires knowing the infrastructure topology, the recent change history, and the operational context. That knowledge lived in the heads of the people who just got walked out with a cardboard box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Incident Rate Nobody Mentions on Earnings Calls
&lt;/h2&gt;

&lt;p&gt;Here's the metric that should be on every board slide but mysteriously isn't. These numbers are composited from publicly shared engineering metrics and postmortem trends across mid-to-large tech companies in late 2025 and early 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Pre-AI Tooling&lt;/th&gt;
&lt;th&gt;Post-AI Tooling&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRs merged per sprint&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;+50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incidents per PR&lt;/td&gt;
&lt;td&gt;0.034&lt;/td&gt;
&lt;td&gt;0.042&lt;/td&gt;
&lt;td&gt;+23.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total incidents per sprint&lt;/td&gt;
&lt;td&gt;1.16&lt;/td&gt;
&lt;td&gt;2.14&lt;/td&gt;
&lt;td&gt;+85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean time to resolve&lt;/td&gt;
&lt;td&gt;2.1 hrs&lt;/td&gt;
&lt;td&gt;3.4 hrs&lt;/td&gt;
&lt;td&gt;+62%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that bottom row. Incidents aren't just happening more often. They're taking longer to fix. Because the people who understood the systems well enough to fix them quickly are gone.&lt;/p&gt;

&lt;p&gt;The math tells a brutal story. A team that shipped 34 PRs and handled ~1.2 incidents per sprint now ships 51 PRs and handles ~2.1 incidents. If each incident consumes an average of 3.4 engineer-hours (up from 2.1), that's 7.1 hours per sprint burned on incidents instead of 2.4.&lt;/p&gt;

&lt;p&gt;The "productivity gain" of 17 extra PRs costs 4.7 extra hours of firefighting per sprint — and that's &lt;em&gt;before&lt;/em&gt; accounting for the reduced team size. Fewer engineers splitting more incident load means each person carries a heavier on-call burden. Burnout follows. Then attrition. Then more hiring, often at premium rates, because the company now needs to backfill institutional knowledge it threw away for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stock Price Play
&lt;/h2&gt;

&lt;p&gt;There's a less charitable explanation for what's happening, and it involves looking at who benefits from the "AI efficiency" story.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a company announces AI-driven layoffs, its stock typically jumps 3-7% within 48 hours. The productivity gains don't need to be real. The market reaction is real enough.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This creates a perverse incentive loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CEO announces layoffs, cites AI efficiency&lt;/li&gt;
&lt;li&gt;Stock jumps&lt;/li&gt;
&lt;li&gt;Executive compensation (tied to stock price) increases&lt;/li&gt;
&lt;li&gt;Board sees stock performance, approves more of the same&lt;/li&gt;
&lt;li&gt;Actual engineering output degrades quietly over 12-18 months&lt;/li&gt;
&lt;li&gt;By the time the damage shows, the narrative has shifted&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's not conspiracy thinking. It's just recognizing that the people making layoff decisions are financially rewarded for making them, regardless of the engineering outcome.&lt;/p&gt;

&lt;p&gt;There's also the uncomfortable overlap between companies &lt;em&gt;selling&lt;/em&gt; AI products and companies &lt;em&gt;citing&lt;/em&gt; AI as the reason for layoffs. When a cloud provider says "AI makes developers 55% more productive," that provider is also selling AI developer tools at $19-40/seat/month. The incentive to produce honest productivity assessments is... limited.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Skill Paradox That Makes It Worse
&lt;/h2&gt;

&lt;p&gt;Here's the part that would be funny if it weren't so destructive.&lt;/p&gt;

&lt;p&gt;Senior engineers get the most value out of AI tools. They know what good code looks like, so they can evaluate AI output critically. Architectural context lets them integrate generated code without breaking existing systems. Years of production failures give them the instinct to reject AI suggestions that will cause problems downstream.&lt;/p&gt;

&lt;p&gt;Junior engineers, by contrast, tend to accept AI suggestions at face value. Without that scar tissue, a clean-looking retry loop reads as correct code rather than a double-charge waiting to happen.&lt;/p&gt;

&lt;p&gt;So the layoffs that disproportionately hit experienced engineers are removing exactly the people who make AI tools safe to use. The remaining team is less equipped to evaluate AI output, which means more bad code ships, which means more incidents, which means more pressure on an already-thinned team.&lt;/p&gt;

&lt;p&gt;It's a doom loop with a nice PowerPoint attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Historical Rhyme
&lt;/h2&gt;

&lt;p&gt;This pattern isn't new. The technology changes. The corporate behavior doesn't.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hype Cycle&lt;/th&gt;
&lt;th&gt;Promise&lt;/th&gt;
&lt;th&gt;Premature Workforce Action&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blockchain (2017-2019)&lt;/td&gt;
&lt;td&gt;Decentralize everything&lt;/td&gt;
&lt;td&gt;Restructured teams for "Web3"&lt;/td&gt;
&lt;td&gt;Most projects dead by 2022&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPA (2015-2020)&lt;/td&gt;
&lt;td&gt;Automate all back-office work&lt;/td&gt;
&lt;td&gt;Cut operations staff&lt;/td&gt;
&lt;td&gt;Rehired 60-70% within 2 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud migration (2012-2016)&lt;/td&gt;
&lt;td&gt;Zero ops engineers needed&lt;/td&gt;
&lt;td&gt;Cut infrastructure teams&lt;/td&gt;
&lt;td&gt;Created "DevOps" role at 2x salary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offshoring (2003-2008)&lt;/td&gt;
&lt;td&gt;Same quality, 1/4 the cost&lt;/td&gt;
&lt;td&gt;Cut domestic engineering&lt;/td&gt;
&lt;td&gt;Brought much of it back within 3-5 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI coding tools (2024-now)&lt;/td&gt;
&lt;td&gt;Replace junior/mid engineers&lt;/td&gt;
&lt;td&gt;Cutting 15-20% of eng orgs&lt;/td&gt;
&lt;td&gt;TBD (but early data isn't encouraging)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All of them followed the same arc: real technology gets overhyped, executives make premature workforce decisions, reality bites, and the company quietly rebuilds — usually at higher cost and with less institutional context.&lt;/p&gt;

&lt;p&gt;The difference this time is that AI coding tools &lt;em&gt;are&lt;/em&gt; useful -- more useful than blockchain was for most businesses, more practical than early RPA. That kernel of real value makes the hype harder to separate from reality. But "useful tool" and "replacement for human judgment" aren't even in the same category.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Smart Companies Are Actually Doing
&lt;/h2&gt;

&lt;p&gt;Not every company is sleepwalking into this trap. The ones getting it right aren't cutting engineers. They're redeploying them.&lt;/p&gt;

&lt;p&gt;The rational approach looks like this: use AI to automate the boring parts (boilerplate, test scaffolding, documentation drafts), then redirect human time toward the work AI can't do (system design, cross-team architecture, security auditing, the kind of deep debugging that requires knowing &lt;em&gt;why&lt;/em&gt; the system is built the way it is).&lt;/p&gt;

&lt;p&gt;A team of 10 using AI well can operate like a team of 14. That's real and valuable. But cutting the team to 7 and hoping AI makes up the difference isn't the same math. One approach builds on proven capability. The other is a coin flip with people's livelihoods.&lt;/p&gt;

&lt;p&gt;The problem is optics. "We're redeploying our workforce to focus on higher-value work alongside AI tools" doesn't pop on an earnings call the way "we cut 20% of engineering and invested in AI" does. Wall Street rewards the dramatic narrative. The sensible one gets polite nods and no stock bump.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 18-Month Prediction
&lt;/h2&gt;

&lt;p&gt;This story has a predictable ending because it's the same ending every time.&lt;/p&gt;

&lt;p&gt;Within 18-24 months, some of the companies making the deepest AI-motivated cuts will be hiring again. The job titles will be different. "AI Systems Engineer" instead of "Software Engineer." "Human-in-the-Loop Architect" instead of "Senior Developer." The LinkedIn posts will celebrate the exciting new roles without mentioning they're doing the same work the laid-off engineers did, minus three years of institutional knowledge.&lt;/p&gt;

&lt;p&gt;The 52,050 people affected in Q1 2026 will mostly land on their feet. Good engineers always do. But the disruption, the uprooted families, the abandoned projects — all of it happened because of a speculative thesis, not empirical evidence.&lt;/p&gt;

&lt;p&gt;Companies are firing developers today based on tools they hope to have tomorrow. The HBR piece named it precisely. These layoffs aren't a response to demonstrated AI capability. They're a response to a &lt;em&gt;story&lt;/em&gt; about AI capability. And unlike software, stories don't need to compile to be believed.&lt;/p&gt;

&lt;p&gt;The data says one thing. The earnings calls say another. Somewhere between those two realities, 52,050 engineers are updating their resumes because a slide deck full of projections was more persuasive than their actual output.&lt;/p&gt;

&lt;p&gt;That's not strategy. That's a very expensive way to learn a lesson the industry has already learned four times before.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Has your team been affected by AI-justified layoffs or restructuring? Are you seeing the productivity gains that leadership claims, or is the gap between the narrative and reality as wide as the data suggests? I'd like to hear what it looks like from the inside.&lt;/strong&gt;&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My project:&lt;/strong&gt; &lt;a href="https://hermes-ide.com" rel="noopener noreferrer"&gt;Hermes IDE&lt;/a&gt; | &lt;a href="https://github.com/hermes-hq/hermes-ide" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://github.com/gabrielanhaia" rel="noopener noreferrer"&gt;gabrielanhaia&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>discuss</category>
      <category>news</category>
    </item>
  </channel>
</rss>
